1
0
pretty/analyze_structure.lua

266 lines
7.8 KiB
Lua

-- pretty.analyze_structure
-- The datastructure analyzing module for pretty.
--------------------------------------------------------------------------------
local TABLE_TYPE
do
local thispath, was_loaded = ... and select('1', ...):match('.+%.') or ''
was_loaded, TABLE_TYPE = pcall(require, thispath..'table_type')
assert(was_loaded, '[pretty]: Could not load vital library: table_type')
end
local RESERVED_LUA_WORDS = {
['and'] = true,
['break'] = true,
['do'] = true,
['else'] = true,
['elseif'] = true,
['end'] = true,
['false'] = true,
['for'] = true,
['function'] = true,
['if'] = true,
['in'] = true,
['local'] = true,
['nil'] = true,
['not'] = true,
['or'] = true,
['repeat'] = true,
['return'] = true,
['then'] = true,
['true'] = true,
['until'] = true,
['while'] = true,
['goto'] = true,
}
local SIMPLE_VALUE_TYPES = {
['nil'] = true,
['boolean'] = true,
['number'] = true,
}
local SHORT_STRING_MAX_LEN = 7
local MINIMUM_NUMBER_OF_SET_ELEMENTS = 2
--------------------------------------------------------------------------------
local function is_identifier(str)
-- An identier is defined in the lua reference guide
return str:match('^[_%a][_%w]*$') and not RESERVED_LUA_WORDS[str]
end
local function get_key_types (t)
local types = { nr_types = -1 }
for key, _ in pairs(t) do
types[type(key)] = true
end
--
for type_name, _ in pairs(types) do
types.nr_types = types.nr_types + 1
end
return types
end
local function get_value_types (t)
local types = { nr_types = -1 }
for _, value in pairs(t) do
types[type(value)] = true
end
--
for type_name, _ in pairs(types) do
types.nr_types = types.nr_types + 1
end
return types
end
local function largest_number_index (t)
-- Returns the largest number index in t.
local max_index = 0
for k,v in pairs(t) do
if type(k) == 'number' then
max_index = math.max(max_index, k)
end
end
return max_index
end
local function nr_elements_in_table (t)
local k, count = nil, -1
repeat
k, count = next(t, k), count + 1
until k == nil
return count
end
local function nr_elements_in_seq (t)
assert(type(t) == 'table')
if getmetatable(t) and getmetatable(t).__index then
return 0, false -- FIXME: Temporary stopgap for when __index throws an
-- error. I think we need to clone the numbers part of the table, to
-- fix this.
end
local i, last_elem_i, nr_elems, has_holes = 0, 0, 0, false
while i <= last_elem_i + 2 do
i = i + 1
if t[i] ~= nil then
last_elem_i, nr_elems = i, nr_elems + 1
else
has_holes = true
end
end
return nr_elems, has_holes
end
--------------------------------------------------------------------------------
local function contains_only_nice_string_keys (t)
-- Predicate: Does t contain only string keys which could be used as
-- identifiers, eg.
for k, _ in pairs(t) do
if type(k) ~= 'string' or not is_identifier(k) then
return false
end
end
return true
end
local function is_set (t)
-- Predicate: Does t contain only boolean values.
local value_types = get_value_types(t)
return value_types.boolean and value_types.nr_types == 1
end
local function is_tabular (t)
-- Predicate: Does t contain sub-tables with identical substructure.
-- Quick return if table is empty, or not conctaining only values of type table.
local value_types = get_value_types(t)
if not value_types.table or value_types.nr_types ~= 1 then
return false
end
-- Determine keys of first child.
local children_keys = {}
for key, _ in pairs(t[next(t)]) do
children_keys[key] = true
end
-- Make sure every other child has exact same sub-structure.
for _, child in pairs(t) do
for key, _ in pairs(children_keys) do
if not child[key] then return false end
end
for key, _ in pairs(child) do
if not children_keys[key] then return false end
end
end
return children_keys
end
local function has_uniform_structure (t)
-- TODO: This can probably be more relaxed. Maybe combine string, number and boolean?
assert(type(t) == 'table')
-- Find the key and value types.
local first_key = next(t)
if first_key == nil then return true end
local key_type, value_type = type(first_key), type(t[first_key])
-- Ensure every other key value pair is the same.
for key, value in pairs(t) do
if type(key) ~= key_type or type(value) ~= value_type then
return false
end
end
return true
end
--------------------------------------------------------------------------------
local function get_table_info (t)
local key_types = get_key_types(t)
local info = {}
info.address = tostring(t):sub(8)
info.nr_elems = nr_elements_in_table(t)
info.seq_elems, info.has_holes = nr_elements_in_seq(t)
info.map_elems = info.nr_elems - info.seq_elems
info.has_seq = info.seq_elems > 0
info.has_map = info.map_elems > 0
info.is_set = is_set(t) and info.nr_elems >= MINIMUM_NUMBER_OF_SET_ELEMENTS
info.is_tabular = is_tabular(t)
info.is_uniform = has_uniform_structure(t)
-- Determine type of table
if not info.has_seq and not info.has_map then info.type = TABLE_TYPE.EMPTY
elseif info.has_seq and not info.has_map then info.type = TABLE_TYPE.SEQUENCE
elseif info.is_set then info.type = TABLE_TYPE.SET
elseif info.has_seq then info.type = TABLE_TYPE.MIXED
elseif contains_only_nice_string_keys(t) then info.type = TABLE_TYPE.STRING_MAP
else info.type = TABLE_TYPE.PURE_MAP
end
return info
end
--------------------------------------------------------------------------------
local function analyze_structure (root, max_depth, info)
-- Argument fixing
local info = info or {}
local max_depth = max_depth or math.huge
-- Quick return
if type(root) ~= 'table' then return info end
-- Error checking
assert(type(root) == 'table')
if type(max_depth) ~= 'number' then error(('[pretty/internal]: Bad argument #2, expected number, got %s (%s)'):format(max_depth, type(max_depth)), 2) end
info.root = info.root or root
local visited, next_mark, depth = {}, 1, { [root] = 0 }
local queue = { root, bottom = 1, top = 2 }
while queue.bottom < queue.top do
queue.bottom = queue.bottom + 1
local node = queue[queue.bottom-1]
-- Who've been visited? Bookkeeping
visited[node], info[node] = (visited[node] or 0) + 1, info[node] or get_table_info(node)
if visited[node] == 2 then
info[node].marker, next_mark = next_mark, next_mark + 1
end
-- Get table info & visit children.
if visited[node] < 2 and depth[node] < max_depth then
for k, v in pairs(node) do
if type(k) == 'table' then queue[queue.top], queue.top, depth[k] = k, queue.top + 1, math.min(depth[k] or math.huge, depth[node] + 1) end
if type(v) == 'table' then queue[queue.top], queue.top, depth[v] = v, queue.top + 1, math.min(depth[v] or math.huge, depth[node] + 1) end
end
end
end
-- Use depth collected
for node in pairs(depth) do
info[node].depth = depth[node]
info[node].address = info[node].address .. ': ' .. info[node].depth
end
assert(type(info) == 'table')
return info
end
--------------------------------------------------------------------------------
return analyze_structure