l-table.lua / last modification: 2020-01-30 14:16
if not modules then modules = { } end modules ['l-table'] = {
    version   = 1.001,
    comment   = "companion to luat-lib.mkiv",
    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
    copyright = "PRAGMA ADE / ConTeXt Development Team",
    license   = "see context related readme files"
}

local type, next, tostring, tonumber, select, rawget = type, next, tostring, tonumber, select, rawget
local table, string = table, string
local concat, sort = table.concat, table.sort
local format, lower, dump = string.format, string.lower, string.dump
local getmetatable, setmetatable = getmetatable, setmetatable
local lpegmatch, patterns = lpeg.match, lpeg.patterns
local floor = math.floor

-- extra functions, some might go (when not used)
--
-- we could serialize using %a but that won't work well is in the code we mostly use
-- floats and as such we get unequality e.g. in version comparisons

local stripper = patterns.stripper

function table.getn(t)
    return t and #t -- for very old times sake
end

function table.strip(tab)
    local lst = { }
    local l   = 0
    for i=1,#tab do
        local s = lpegmatch(stripper,tab[i]) or ""
        if s == "" then
            -- skip this one
        else
            l = l + 1
            lst[l] = s
        end
    end
    return lst
end

function table.keys(t)
    if t then
        local keys = { }
        local k    = 0
        for key in next, t do
            k = k + 1
            keys[k] = key
        end
        return keys
    else
        return { }
    end
end

-- local function compare(a,b)
--     local ta = type(a) -- needed, else 11 < 2
--     local tb = type(b) -- needed, else 11 < 2
--     if ta == tb and ta == "number" then
--         return a < b
--     else
--         return tostring(a) < tostring(b) -- not that efficient
--     end
-- end

-- local function compare(a,b)
--     local ta = type(a) -- needed, else 11 < 2
--     local tb = type(b) -- needed, else 11 < 2
--     if ta == tb and (ta == "number" or ta == "string") then
--         return a < b
--     else
--         return tostring(a) < tostring(b) -- not that efficient
--     end
-- end

-- local function sortedkeys(tab)
--     if tab then
--         local srt, category, s = { }, 0, 0 -- 0=unknown 1=string, 2=number 3=mixed
--         for key in next, tab do
--             s = s + 1
--             srt[s] = key
--             if category == 3 then
--                 -- no further check
--             else
--                 local tkey = type(key)
--                 if tkey == "string" then
--                     category = (category == 2 and 3) or 1
--                 elseif tkey == "number" then
--                     category = (category == 1 and 3) or 2
--                 else
--                     category = 3
--                 end
--             end
--         end
--         if category == 0 or category == 3 then
--             sort(srt,compare)
--         else
--             sort(srt)
--         end
--         return srt
--     else
--         return { }
--     end
-- end

-- local function compare(a,b)
--     local ta = type(a) -- needed, else 11 < 2
--     local tb = type(b) -- needed, else 11 < 2
--     if ta == tb and (ta == "number" or ta == "string") then
--         return a < b
--     else
--         return tostring(a) < tostring(b) -- not that efficient
--     end
-- end

-- local function compare(a,b)
--     local ta = type(a) -- needed, else 11 < 2
--     if ta == "number" or ta == "string" then
--         local tb = type(b) -- needed, else 11 < 2
--         if ta == tb then
--             return a < b
--         end
--     end
--     return tostring(a) < tostring(b) -- not that efficient
-- end

local function compare(a,b)
    local ta = type(a) -- needed, else 11 < 2
    if ta == "number" then
        local tb = type(b) -- needed, else 11 < 2
        if ta == tb then
            return a < b
        elseif tb == "string" then
            return tostring(a) < b
        end
    elseif ta == "string" then
        local tb = type(b) -- needed, else 11 < 2
        if ta == tb then
            return a < b
        else
            return a < tostring(b)
        end
    end
    return tostring(a) < tostring(b) -- not that efficient
end

local function sortedkeys(tab)
    if tab then
        local srt      = { }
        local category = 0 -- 0=unknown 1=string, 2=number 3=mixed
        local s        = 0
        for key in next, tab do
            s = s + 1
            srt[s] = key
            if category ~= 3 then
                local tkey = type(key)
                if category == 1 then
                    if tkey ~= "string" then
                        category = 3
                    end
                elseif category == 2 then
                    if tkey ~= "number" then
                        category = 3
                    end
                else
                    if tkey == "string" then
                        category = 1
                    elseif tkey == "number" then
                        category = 2
                    else
                        category = 3
                    end
                end
            end
        end
        if s < 2 then
            -- nothing to sort
        elseif category == 3 then
            sort(srt,compare)
        else
            sort(srt)
        end
        return srt
    else
        return { }
    end
end

local function sortedhashonly(tab)
    if tab then
        local srt = { }
        local s   = 0
        for key in next, tab do
            if type(key) == "string" then
                s = s + 1
                srt[s] = key
            end
        end
        if s > 1 then
            sort(srt)
        end
        return srt
    else
        return { }
    end
end

local function sortedindexonly(tab)
    if tab then
        local srt = { }
        local s   = 0
        for key in next, tab do
            if type(key) == "number" then
                s = s + 1
                srt[s] = key
            end
        end
        if s > 1 then
            sort(srt)
        end
        return srt
    else
        return { }
    end
end

local function sortedhashkeys(tab,cmp) -- fast one
    if tab then
        local srt = { }
        local s   = 0
        for key in next, tab do
            if key then
                s= s + 1
                srt[s] = key
            end
        end
        if s > 1 then
            sort(srt,cmp)
        end
        return srt
    else
        return { }
    end
end

function table.allkeys(t)
    local keys = { }
    for k, v in next, t do
        for k in next, v do
            keys[k] = true
        end
    end
    return sortedkeys(keys)
end

table.sortedkeys      = sortedkeys
table.sortedhashonly  = sortedhashonly
table.sortedindexonly = sortedindexonly
table.sortedhashkeys  = sortedhashkeys

local function nothing() end

local function sortedhash(t,cmp)
    if t then
        local s
        if cmp then
            -- it would be nice if the sort function would accept a third argument (or nicer, an optional first)
            s = sortedhashkeys(t,function(a,b) return cmp(t,a,b) end)
        else
            s = sortedkeys(t) -- the robust one
        end
        local m = #s
        if m == 1 then
            return next, t
        elseif m > 0 then
            local n = 0
            return function()
                if n < m then
                    n = n + 1
                    local k = s[n]
                    return k, t[k]
                end
            end
        end
    end
    return nothing
end

-- local function iterate(t,i)
--     local i = i + 1
--     if i <= t.n then
--         local k = t[i]
--         return i, k, t.t[k]
--     end
-- end
--
-- local function indexedhash(t,cmp)
--     if t then
--         local s
--         if cmp then
--             -- it would be nice if the sort function would accept a third argument (or nicer, an optional first)
--             s = sortedhashkeys(t,function(a,b) return cmp(t,a,b) end)
--         else
--             s = sortedkeys(t) -- the robust one
--         end
--         local m = #s
--         if m == 1 then
--             return next, t
--         elseif m > 0 then
--             s.n = m
--             s.t = t
--             return iterate, s, 0
--         end
--     end
--     return nothing
-- end
--
-- -- for i, k, v in indexedhash(t) do print(k,v,s) end

table.sortedhash  = sortedhash
table.sortedpairs = sortedhash -- obsolete

function table.append(t,list)
    local n = #t
    for i=1,#list do
        n = n + 1
        t[n] = list[i]
    end
    return t
end

function table.prepend(t, list)
    local nl = #list
    local nt = nl + #t
    for i=#t,1,-1 do
        t[nt] = t[i]
        nt = nt - 1
    end
    for i=1,#list do
        t[i] = list[i]
    end
    return t
end

-- function table.merge(t, ...) -- first one is target
--     t = t or { }
--     local lst = { ... }
--     for i=1,#lst do
--         for k, v in next, lst[i] do
--             t[k] = v
--         end
--     end
--     return t
-- end

function table.merge(t, ...) -- first one is target
    if not t then
        t = { }
    end
    for i=1,select("#",...) do
        for k, v in next, (select(i,...)) do
            t[k] = v
        end
    end
    return t
end

-- function table.merged(...)
--     local tmp, lst = { }, { ... }
--     for i=1,#lst do
--         for k, v in next, lst[i] do
--             tmp[k] = v
--         end
--     end
--     return tmp
-- end

function table.merged(...)
    local t = { }
    for i=1,select("#",...) do
        for k, v in next, (select(i,...)) do
            t[k] = v
        end
    end
    return t
end

-- function table.imerge(t, ...)
--     local lst, nt = { ... }, #t
--     for i=1,#lst do
--         local nst = lst[i]
--         for j=1,#nst do
--             nt = nt + 1
--             t[nt] = nst[j]
--         end
--     end
--     return t
-- end

function table.imerge(t, ...)
    local nt = #t
    for i=1,select("#",...) do
        local nst = select(i,...)
        for j=1,#nst do
            nt = nt + 1
            t[nt] = nst[j]
        end
    end
    return t
end

-- function table.imerged(...)
--     local tmp, ntmp, lst = { }, 0, {...}
--     for i=1,#lst do
--         local nst = lst[i]
--         for j=1,#nst do
--             ntmp = ntmp + 1
--             tmp[ntmp] = nst[j]
--         end
--     end
--     return tmp
-- end

function table.imerged(...)
    local tmp  = { }
    local ntmp = 0
    for i=1,select("#",...) do
        local nst = select(i,...)
        for j=1,#nst do
            ntmp = ntmp + 1
            tmp[ntmp] = nst[j]
        end
    end
    return tmp
end

local function fastcopy(old,metatabletoo) -- fast one
    if old then
        local new = { }
        for k, v in next, old do
            if type(v) == "table" then
                new[k] = fastcopy(v,metatabletoo) -- was just table.copy
            else
                new[k] = v
            end
        end
        if metatabletoo then
            -- optional second arg
            local mt = getmetatable(old)
            if mt then
                setmetatable(new,mt)
            end
        end
        return new
    else
        return { }
    end
end

-- todo : copy without metatable

local function copy(t,tables) -- taken from lua wiki, slightly adapted
    if not tables then
        tables = { }
    end
    local tcopy = { }
    if not tables[t] then
        tables[t] = tcopy
    end
    for i,v in next, t do -- brrr, what happens with sparse indexed
        if type(i) == "table" then
            if tables[i] then
                i = tables[i]
            else
                i = copy(i,tables)
            end
        end
        if type(v) ~= "table" then
            tcopy[i] = v
        elseif tables[v] then
            tcopy[i] = tables[v]
        else
            tcopy[i] = copy(v,tables)
        end
    end
    local mt = getmetatable(t)
    if mt then
        setmetatable(tcopy,mt)
    end
    return tcopy
end

table.fastcopy = fastcopy
table.copy     = copy

function table.derive(parent) -- for the moment not public
    local child = { }
    if parent then
        setmetatable(child,{ __index = parent })
    end
    return child
end

function table.tohash(t,value)
    local h = { }
    if t then
        if value == nil then value = true end
        for _, v in next, t do
            h[v] = value
        end
    end
    return h
end

function table.fromhash(t)
    local hsh = { }
    local h   = 0
    for k, v in next, t do
        if v then
            h = h + 1
            hsh[h] = k
        end
    end
    return hsh
end

local noquotes, hexify, handle, compact, inline, functions, metacheck

local reserved = table.tohash { -- intercept a language inconvenience: no reserved words as key
    'and', 'break', 'do', 'else', 'elseif', 'end', 'false', 'for', 'function', 'if',
    'in', 'local', 'nil', 'not', 'or', 'repeat', 'return', 'then', 'true', 'until', 'while',
    'NaN', 'goto',
}

-- local function is_simple_table(t)
--     if #t > 0 then
--         local n = 0
--         for _,v in next, t do
--             n = n + 1
--         end
--         if n == #t then
--             local tt, nt = { }, 0
--             for i=1,#t do
--                 local v = t[i]
--                 local tv = type(v)
--                 if tv == "number" then
--                     nt = nt + 1
--                     if hexify then
--                         tt[nt] = format("0x%X",v)
--                     else
--                         tt[nt] = tostring(v) -- tostring not needed
--                     end
--                 elseif tv == "string" then
--                     nt = nt + 1
--                     tt[nt] = format("%q",v)
--                 elseif tv == "boolean" then
--                     nt = nt + 1
--                     tt[nt] = v and "true" or "false"
--                 else
--                     return nil
--                 end
--             end
--             return tt
--         end
--     end
--     return nil
-- end

-- local function is_simple_table(t)
--     local nt = #t
--     if nt > 0 then
--         local n = 0
--         for _,v in next, t do
--             n = n + 1
--          -- if type(v) == "table" then
--          --     return nil
--          -- end
--         end
--         if n == nt then
--             local tt = { }
--             for i=1,nt do
--                 local v = t[i]
--                 local tv = type(v)
--                 if tv == "number" then
--                     if hexify then
--                         tt[i] = format("0x%X",v)
--                     else
--                         tt[i] = tostring(v) -- tostring not needed
--                     end
--                 elseif tv == "string" then
--                     tt[i] = format("%q",v)
--                 elseif tv == "boolean" then
--                     tt[i] = v and "true" or "false"
--                 else
--                     return nil
--                 end
--             end
--             return tt
--         end
--     end
--     return nil
-- end

local function is_simple_table(t,hexify) -- also used in util-tab so maybe public
    local nt = #t
    if nt > 0 then
        local n = 0
        for _, v in next, t do
            n = n + 1
            if type(v) == "table" then
                return nil
            end
        end
     -- local haszero = t[0]
        local haszero = rawget(t,0) -- don't trigger meta
        if n == nt then
            local tt = { }
            for i=1,nt do
                local v = t[i]
                local tv = type(v)
                if tv == "number" then
                 -- tt[i] = v -- not needed tostring(v)
                    if hexify then
                        tt[i] = format("0x%X",v)
                    else
                        tt[i] = v -- not needed tostring(v)
                    end
                elseif tv == "string" then
                    tt[i] = format("%q",v) -- f_string(v)
                elseif tv == "boolean" then
                    tt[i] = v and "true" or "false"
                else
                    return nil
                end
            end
            return tt
        elseif haszero and (n == nt + 1) then
            local tt = { }
            for i=0,nt do
                local v = t[i]
                local tv = type(v)
                if tv == "number" then
                 -- tt[i+1] = v -- not needed tostring(v)
                    if hexify then
                        tt[i+1] = format("0x%X",v)
                    else
                        tt[i+1] = v -- not needed tostring(v)
                    end
                elseif tv == "string" then
                    tt[i+1] = format("%q",v) -- f_string(v)
                elseif tv == "boolean" then
                    tt[i+1] = v and "true" or "false"
                else
                    return nil
                end
            end
            tt[1] = "[0] = " .. tt[1]
            return tt
        end
    end
    return nil
end

table.is_simple_table = is_simple_table

-- Because this is a core function of mkiv I moved some function calls
-- inline.
--
-- twice as fast in a test:
--
-- local propername = lpeg.P(lpeg.R("AZ","az","__") * lpeg.R("09","AZ","az", "__")^0 * lpeg.P(-1) )

-- problem: there no good number_to_string converter with the best resolution

-- probably using .. is faster than format
-- maybe split in a few cases (yes/no hexify)

-- todo: %g faster on numbers than %s

-- we can speed this up with repeaters and formatters but we haven't defined them
-- yet

local propername = patterns.propername -- was find(name,"^%a[%w%_]*$")

local function dummy() end

local function do_serialize(root,name,depth,level,indexed)
    if level > 0 then
        depth = depth .. " "
        if indexed then
            handle(format("%s{",depth))
        else
            local tn = type(name)
            if tn == "number" then
                if hexify then
                    handle(format("%s[0x%X]={",depth,name))
                else
                    handle(format("%s[%s]={",depth,name))
                end
            elseif tn == "string" then
                if noquotes and not reserved[name] and lpegmatch(propername,name) then
                    handle(format("%s%s={",depth,name))
                else
                    handle(format("%s[%q]={",depth,name))
                end
            elseif tn == "boolean" then
                handle(format("%s[%s]={",depth,name and "true" or "false"))
            else
                handle(format("%s{",depth))
            end
        end
    end
    -- we could check for k (index) being number (cardinal)
    if root and next(root) ~= nil then
        local first = nil
        local last  = 0
        if compact then
            last = #root
            for k=1,last do
             -- if root[k] == nil then
                if rawget(root,k) == nil then
                    last = k - 1
                    break
                end
            end
            if last > 0 then
                first = 1
            end
        end
        local sk = sortedkeys(root)
        for i=1,#sk do
            local k  = sk[i]
            local v  = root[k]
            local tv = type(v)
            local tk = type(k)
            if compact and first and tk == "number" and k >= first and k <= last then
                if tv == "number" then
                    if hexify then
                        handle(format("%s 0x%X,",depth,v))
                    else
                        handle(format("%s %s,",depth,v)) -- %.99g
                    end
                elseif tv == "string" then
                    handle(format("%s %q,",depth,v))
                elseif tv == "table" then
                    if next(v) == nil then
                        handle(format("%s {},",depth))
                    elseif inline then -- and #t > 0
                        local st = is_simple_table(v,hexify)
                        if st then
                            handle(format("%s { %s },",depth,concat(st,", ")))
                        else
                            do_serialize(v,k,depth,level+1,true)
                        end
                    else
                        do_serialize(v,k,depth,level+1,true)
                    end
                elseif tv == "boolean" then
                    handle(format("%s %s,",depth,v and "true" or "false"))
                elseif tv == "function" then
                    if functions then
                        handle(format('%s load(%q),',depth,dump(v))) -- maybe strip
                    else
                        handle(format('%s "function",',depth))
                    end
                else
                    handle(format("%s %q,",depth,tostring(v)))
                end
            elseif k == "__p__" then -- parent
                if false then
                    handle(format("%s __p__=nil,",depth))
                end
            elseif tv == "number" then
                if tk == "number" then
                    if hexify then
                        handle(format("%s [0x%X]=0x%X,",depth,k,v))
                    else
                        handle(format("%s [%s]=%s,",depth,k,v)) -- %.99g
                    end
                elseif tk == "boolean" then
                    if hexify then
                        handle(format("%s [%s]=0x%X,",depth,k and "true" or "false",v))
                    else
                        handle(format("%s [%s]=%s,",depth,k and "true" or "false",v)) -- %.99g
                    end
                elseif tk ~= "string" then
                    -- ignore
                elseif noquotes and not reserved[k] and lpegmatch(propername,k) then
                    if hexify then
                        handle(format("%s %s=0x%X,",depth,k,v))
                    else
                        handle(format("%s %s=%s,",depth,k,v)) -- %.99g
                    end
                else
                    if hexify then
                        handle(format("%s [%q]=0x%X,",depth,k,v))
                    else
                        handle(format("%s [%q]=%s,",depth,k,v)) -- %.99g
                    end
                end
            elseif tv == "string" then
                if tk == "number" then
                    if hexify then
                        handle(format("%s [0x%X]=%q,",depth,k,v))
                    else
                        handle(format("%s [%s]=%q,",depth,k,v))
                    end
                elseif tk == "boolean" then
                    handle(format("%s [%s]=%q,",depth,k and "true" or "false",v))
                elseif tk ~= "string" then
                    -- ignore
                elseif noquotes and not reserved[k] and lpegmatch(propername,k) then
                    handle(format("%s %s=%q,",depth,k,v))
                else
                    handle(format("%s [%q]=%q,",depth,k,v))
                end
            elseif tv == "table" then
                if next(v) == nil then
                    if tk == "number" then
                        if hexify then
                            handle(format("%s [0x%X]={},",depth,k))
                        else
                            handle(format("%s [%s]={},",depth,k))
                        end
                    elseif tk == "boolean" then
                        handle(format("%s [%s]={},",depth,k and "true" or "false"))
                    elseif tk ~= "string" then
                        -- ignore
                    elseif noquotes and not reserved[k] and lpegmatch(propername,k) then
                        handle(format("%s %s={},",depth,k))
                    else
                        handle(format("%s [%q]={},",depth,k))
                    end
                elseif inline then
                    local st = is_simple_table(v,hexify)
                    if st then
                        if tk == "number" then
                            if hexify then
                                handle(format("%s [0x%X]={ %s },",depth,k,concat(st,", ")))
                            else
                                handle(format("%s [%s]={ %s },",depth,k,concat(st,", ")))
                            end
                        elseif tk == "boolean" then
                            handle(format("%s [%s]={ %s },",depth,k and "true" or "false",concat(st,", ")))
                        elseif tk ~= "string" then
                            -- ignore
                        elseif noquotes and not reserved[k] and lpegmatch(propername,k) then
                            handle(format("%s %s={ %s },",depth,k,concat(st,", ")))
                        else
                            handle(format("%s [%q]={ %s },",depth,k,concat(st,", ")))
                        end
                    else
                        do_serialize(v,k,depth,level+1)
                    end
                else
                    do_serialize(v,k,depth,level+1)
                end
            elseif tv == "boolean" then
                if tk == "number" then
                    if hexify then
                        handle(format("%s [0x%X]=%s,",depth,k,v and "true" or "false"))
                    else
                        handle(format("%s [%s]=%s,",depth,k,v and "true" or "false"))
                    end
                elseif tk == "boolean" then
                    handle(format("%s [%s]=%s,",depth,tostring(k),v and "true" or "false"))
                elseif tk ~= "string" then
                    -- ignore
                elseif noquotes and not reserved[k] and lpegmatch(propername,k) then
                    handle(format("%s %s=%s,",depth,k,v and "true" or "false"))
                else
                    handle(format("%s [%q]=%s,",depth,k,v and "true" or "false"))
                end
            elseif tv == "function" then
                if functions then
                    local getinfo = debug and debug.getinfo
                    if getinfo then
                        local f = getinfo(v).what == "C" and dump(dummy) or dump(v) -- maybe strip
                     -- local f = getinfo(v).what == "C" and dump(function(...) return v(...) end) or dump(v) -- maybe strip
                        if tk == "number" then
                            if hexify then
                                handle(format("%s [0x%X]=load(%q),",depth,k,f))
                            else
                                handle(format("%s [%s]=load(%q),",depth,k,f))
                            end
                        elseif tk == "boolean" then
                            handle(format("%s [%s]=load(%q),",depth,k and "true" or "false",f))
                        elseif tk ~= "string" then
                            -- ignore
                        elseif noquotes and not reserved[k] and lpegmatch(propername,k) then
                            handle(format("%s %s=load(%q),",depth,k,f))
                        else
                            handle(format("%s [%q]=load(%q),",depth,k,f))
                        end
                    end
                end
            else
                if tk == "number" then
                    if hexify then
                        handle(format("%s [0x%X]=%q,",depth,k,tostring(v)))
                    else
                        handle(format("%s [%s]=%q,",depth,k,tostring(v)))
                    end
                elseif tk == "boolean" then
                    handle(format("%s [%s]=%q,",depth,k and "true" or "false",tostring(v)))
                elseif tk ~= "string" then
                    -- ignore
                elseif noquotes and not reserved[k] and lpegmatch(propername,k) then
                    handle(format("%s %s=%q,",depth,k,tostring(v)))
                else
                    handle(format("%s [%q]=%q,",depth,k,tostring(v)))
                end
            end
        end
    end
    if level > 0 then
        handle(format("%s},",depth))
    end
end

-- replacing handle by a direct t[#t+1] = ... (plus test) is not much
-- faster (0.03 on 1.00 for zapfino.tma)

local function serialize(_handle,root,name,specification) -- handle wins
    local tname = type(name)
    if type(specification) == "table" then
        noquotes  = specification.noquotes
        hexify    = specification.hexify
        handle    = _handle or specification.handle or print
        functions = specification.functions
        compact   = specification.compact
        inline    = specification.inline and compact
        metacheck = specification.metacheck
        if functions == nil then
            functions = true
        end
        if compact == nil then
            compact = true
        end
        if inline == nil then
            inline = compact
        end
        if metacheck == nil then
            metacheck = true
        end
    else
        noquotes  = false
        hexify    = false
        handle    = _handle or print
        compact   = true
        inline    = true
        functions = true
        metacheck = true
    end
    if tname == "string" then
        if name == "return" then
            handle("return {")
        else
            handle(name .. "={")
        end
    elseif tname == "number" then
        if hexify then
            handle(format("[0x%X]={",name))
        else
            handle("[" .. name .. "]={")
        end
    elseif tname == "boolean" then
        if name then
            handle("return {")
        else
            handle("{")
        end
    else
        handle("t={")
    end
    if root then
        -- The dummy access will initialize a table that has a delayed initialization
        -- using a metatable. (maybe explicitly test for metatable). This can crash on
        -- metatables that check the index against a number.
        if metacheck and getmetatable(root) then
            local dummy = root._w_h_a_t_e_v_e_r_
            root._w_h_a_t_e_v_e_r_ = nil
        end
        -- Let's forget about empty tables.
        if next(root) ~= nil then
            do_serialize(root,name,"",0)
        end
    end
    handle("}")
end

-- A version with formatters is some 20% faster than using format (because formatters are
-- much faster) but of course, inlining the format using .. is then again faster .. anyway,
-- as we do some pretty printing as well there is not that much to gain unless we make a
-- 'fast' ugly variant as well. But, we would have to move the formatter to l-string then.

-- name:
--
-- true     : return     { }
-- false    :            { }
-- nil      : t        = { }
-- string   : string   = { }
-- "return" : return     { }
-- number   : [number] = { }

function table.serialize(root,name,specification)
    local t = { }
    local n = 0
    local function flush(s)
        n = n + 1
        t[n] = s
    end
    serialize(flush,root,name,specification)
    return concat(t,"\n")
end

--   local a = { e = { 1,2,3,4,5,6}, a = 1, b = 2, c = "ccc", d = { a = 1, b = 2, c = "ccc", d = { a = 1, b = 2, c = "ccc" } } }
--   local t = os.clock()
--   for i=1,10000 do
--       table.serialize(a)
--   end
--   print(os.clock()-t,table.serialize(a))

table.tohandle = serialize

local maxtab = 2*1024

function table.tofile(filename,root,name,specification)
    local f = io.open(filename,'w')
    if f then
        if maxtab > 1 then
            local t = { }
            local n = 0
            local function flush(s)
                n = n + 1
                t[n] = s
                if n > maxtab then
                    f:write(concat(t,"\n"),"\n") -- hm, write(sometable) should be nice
                    t = { } -- we could recycle t if needed
                    n = 0
                end
            end
            serialize(flush,root,name,specification)
            f:write(concat(t,"\n"),"\n")
        else
            local function flush(s)
                f:write(s,"\n")
            end
            serialize(flush,root,name,specification)
        end
        f:close()
        io.flush()
    end
end

local function flattened(t,f,depth) -- also handles { nil, 1, nil, 2 }
    if f == nil then
        f     = { }
        depth = 0xFFFF
    elseif tonumber(f) then
        -- assume that only two arguments are given
        depth = f
        f     = { }
    elseif not depth then
        depth = 0xFFFF
    end
    for k, v in next, t do
        if type(k) ~= "number" then
            if depth > 0 and type(v) == "table" then
                flattened(v,f,depth-1)
            else
                f[#f+1] = v
            end
        end
    end
    for k=1,#t do
        local v = t[k]
        if depth > 0 and type(v) == "table" then
            flattened(v,f,depth-1)
        else
            f[#f+1] = v
        end
    end
    return f
end

table.flattened = flattened

local function collapsed(t,f,h)
    if f == nil then
        f = { }
        h = { }
    end
    for k=1,#t do
        local v = t[k]
        if type(v) == "table" then
            collapsed(v,f,h)
        elseif not h[v] then
            f[#f+1] = v
            h[v] = true
        end
    end
    return f
end

local function collapsedhash(t,h)
    if h == nil then
        h = { }
    end
    for k=1,#t do
        local v = t[k]
        if type(v) == "table" then
            collapsedhash(v,h)
        else
            h[v] = true
        end
    end
    return h
end

table.collapsed     = collapsed     -- 20% faster than unique(collapsed(t))
table.collapsedhash = collapsedhash

local function unnest(t,f) -- only used in mk, for old times sake
    if not f then          -- and only relevant for token lists
        f = { }            -- this one can become obsolete
    end
    for i=1,#t do
        local v = t[i]
        if type(v) == "table" then
            if type(v[1]) == "table" then
                unnest(v,f)
            else
                f[#f+1] = v
            end
        else
            f[#f+1] = v
        end
    end
    return f
end

function table.unnest(t) -- bad name
    return unnest(t)
end

local function are_equal(a,b,n,m) -- indexed
    if a == b then
        return true
    elseif a and b and #a == #b then
        if not n then
            n = 1
        end
        if not m then
            m = #a
        end
        for i=n,m do
            local ai, bi = a[i], b[i]
            if ai==bi then
                -- same
            elseif type(ai) == "table" and type(bi) == "table" then
                if not are_equal(ai,bi) then
                    return false
                end
            else
                return false
            end
        end
        return true
    else
        return false
    end
end

local function identical(a,b) -- assumes same structure
    if a ~= b then
        for ka, va in next, a do
            local vb = b[ka]
            if va == vb then
                -- same
            elseif type(va) == "table" and  type(vb) == "table" then
                if not identical(va,vb) then
                    return false
                end
            else
                return false
            end
        end
    end
    return true
end

table.identical = identical
table.are_equal = are_equal

local function sparse(old,nest,keeptables)
    local new  = { }
    for k, v in next, old do
        if not (v == "" or v == false) then
            if nest and type(v) == "table" then
                v = sparse(v,nest)
                if keeptables or next(v) ~= nil then
                    new[k] = v
                end
            else
                new[k] = v
            end
        end
    end
    return new
end

table.sparse = sparse

function table.compact(t)
    return sparse(t,true,true)
end

function table.contains(t, v)
    if t then
        for i=1, #t do
            if t[i] == v then
                return i
            end
        end
    end
    return false
end

function table.count(t)
    local n = 0
    for k, v in next, t do
        n = n + 1
    end
    return n
end

function table.swapped(t,s) -- hash, we need to make sure we don't mess up next
    local n = { }
    if s then
        for k, v in next, s do
            n[k] = v
        end
    end
    for k, v in next, t do
        n[v] = k
    end
    return n
end

function table.hashed(t) -- list, add hash to index (save because we are not yet mixed
    for i=1,#t do
        t[t[i]] = i
    end
    return t
end

function table.mirrored(t) -- hash, we need to make sure we don't mess up next
    local n = { }
    for k, v in next, t do
        n[v] = k
        n[k] = v
    end
    return n
end

function table.reversed(t)
    if t then
        local tt = { }
        local tn = #t
        if tn > 0 then
            local ttn = 0
            for i=tn,1,-1 do
                ttn = ttn + 1
                tt[ttn] = t[i]
            end
        end
        return tt
    end
end

function table.reverse(t) -- check with 5.3 ?
    if t then
        local n = #t
        local m = n + 1
        for i=1,floor(n/2) do -- maybe just n//2
            local j = m - i
            t[i], t[j] = t[j], t[i]
        end
        return t
    end
end

-- This one is for really simple cases where need a hash from a table.

local function sequenced(t,sep,simple)
    if not t then
        return ""
    elseif type(t) ~= "table" then
        return t -- handy fallback
    end
    local n = #t
    local s = { }
    if n > 0 then
        -- indexed
        for i=1,n do
            local v = t[i]
            if type(v) == "table" then
                s[i] = "{" .. sequenced(v,sep,simple) .. "}"
            else
                s[i] = tostring(t[i])
            end
        end
    else
        -- hashed
        n = 0
        for k, v in sortedhash(t) do
            if simple then
                if v == true then
                    n = n + 1
                    s[n] = k
                elseif v and v~= "" then
                    n = n + 1
                    if type(v) == "table" then
                        s[n] = k .. "={" .. sequenced(v,sep,simple) .. "}"
                    else
                        s[n] = k .. "=" .. tostring(v)
                    end
                end
            else
                n = n + 1
                if type(v) == "table" then
                    s[n] = k .. "={" .. sequenced(v,sep,simple) .. "}"
                else
                    s[n] = k .. "=" .. tostring(v)
                end
            end
        end
    end
    if sep == true then
        return "{ " .. concat(s,", ") .. " }"
    else
        return concat(s,sep or " | ")
    end
end

table.sequenced = sequenced

function table.print(t,...)
    if type(t) ~= "table" then
        print(tostring(t))
    else
        serialize(print,t,...)
    end
end

if setinspector then
    setinspector("table",function(v) if type(v) == "table" then serialize(print,v,"table") return true end end)
end

-- -- -- obsolete but we keep them for a while and might comment them later -- -- --

-- roughly: copy-loop : unpack : sub == 0.9 : 0.4 : 0.45 (so in critical apps, use unpack)

function table.sub(t,i,j)
    return { unpack(t,i,j) }
end

-- slower than #t on indexed tables (#t only returns the size of the numerically indexed slice)

function table.is_empty(t)
    return not t or next(t) == nil
end

function table.has_one_entry(t)
    return t and next(t,next(t)) == nil
end

-- new (rather basic, not indexed and nested)

function table.loweredkeys(t) -- maybe utf
    local l = { }
    for k, v in next, t do
        l[lower(k)] = v
    end
    return l
end

-- new, might move (maybe duplicate)

function table.unique(old)
    local hash = { }
    local new  = { }
    local n    = 0
    for i=1,#old do
        local oi = old[i]
        if not hash[oi] then
            n = n + 1
            new[n] = oi
            hash[oi] = true
        end
    end
    return new
end

function table.sorted(t,...)
    sort(t,...)
    return t -- still sorts in-place
end

--

function table.values(t,s) -- optional sort flag
    if t then
        local values = { }
        local keys   = { }
        local v      = 0
        for key, value in next, t do
            if not keys[value] then
                v = v + 1
                values[v] = value
                keys[k]   = key
            end
        end
        if s then
            sort(values)
        end
        return values
    else
        return { }
    end
end

-- maybe this will move to util-tab.lua

-- for k, v in table.filtered(t,pattern)          do ... end
-- for k, v in table.filtered(t,pattern,true)     do ... end
-- for k, v in table.filtered(t,pattern,true,cmp) do ... end

function table.filtered(t,pattern,sort,cmp)
    if t and type(pattern) == "string" then
        if sort then
            local s
            if cmp then
                -- it would be nice if the sort function would accept a third argument (or nicer, an optional first)
                s = sortedhashkeys(t,function(a,b) return cmp(t,a,b) end)
            else
                s = sortedkeys(t) -- the robust one
            end
            local n = 0
            local m = #s
            local function kv(s)
                while n < m do
                    n = n + 1
                    local k = s[n]
                    if find(k,pattern) then
                        return k, t[k]
                    end
                end
            end
            return kv, s
        else
            local n = next(t)
            local function iterator()
                while n ~= nil do
                    local k = n
                    n = next(t,k)
                    if find(k,pattern) then
                        return k, t[k]
                    end
                end
            end
            return iterator, t
        end
    else
        return nothing
    end
end

-- lua 5.3:

if not table.move then

    function table.move(a1,f,e,t,a2)
        if a2 and a1 ~= a2 then
            for i=f,e do
                a2[t] = a1[i]
                t = t + 1
            end
            return a2
        else
            t = t + e - f
            for i=e,f,-1 do
                a1[t] = a1[i]
                t = t - 1
            end
            return a1
        end
    end

end