-- generate a graphviz graph from a lua table structure local function append( tab, ... ) for i = 1, select( '#', ... ) do tab[ #tab + 1 ] = (select( i, ... )) end return tab end local function abbrev( str, data ) local escape = "\\\\" if data.use_html then escape = "\\" end local s = string.gsub( str, "[^%w?!=/+*-_.:,; ]", function( c ) -- local s = string.gsub( str, "[^%w_]", function( c ) return escape .. string.byte( c ) end ) if string.len( s ) > 20 then s = string.sub( s, 1, 17 ) .. "..." end return "'" .. s .. "'" end local function update_node_depth( val, data, depth ) data.node2depth[ val ] = math.min( data.node2depth[ val ] or depth, depth ) end local function define_node( data, node ) assert( not data.node2id[ node.value ] ) local id = data.n_nodes data.n_nodes = data.n_nodes + 1 data.node2id[ node.value ] = id append( data.nodes, node ) return id end local function define_edge( data, edge ) append( data.edges, edge ) end local function get_metatable( val, enabled ) if enabled then if type( debug ) == "table" and type( debug.getmetatable ) == "function" then return debug.getmetatable( val ) elseif type( getmetatable ) == "function" then return getmetatable( val ) end end end local function get_environment( val, enabled ) if enabled then if type( debug ) == "table" and type( debug.getfenv ) == "function" then return debug.getfenv( val ) elseif type( getfenv ) == "function" and type( val ) == "function" then return getfenv( val ) end end end -- generate dot code for references local function dottify_metatable_ref( val, id1, mt, id2, data ) append( data.edges, { A = val, A_id = id1, B = mt, B_id = id2, style = "dashed", arrowtail = "odiamond", label = "metatable", color = "blue" } ) data.nodes[ data.node2id[ val ] ].important = true data.nodes[ data.node2id[ mt ] ].important = true end local function dottify_environment_ref( val, id1, env, id2, data ) append( data.edges, { A = val, A_id = id1, B = env, B_id = id2, style = "dotted", arrowtail = "dot", label = "environment", color = "red" } ) data.nodes[ data.node2id[ val ] ].important = true data.nodes[ data.node2id[ env ] ].important = true end local function dottify_upvalue_ref( val, id1, upv, id2, data, name ) append( data.edges, { A = val, A_id = id1, B = upv, B_id = id2, style = "dashed", label = name or "#upvalue", color = "green" } ) data.nodes[ data.node2id[ val ] ].important = true data.nodes[ data.node2id[ upv ] ].important = true end local function dottify_ref( val1, id1, val2, id2, data ) append( data.edges, { A = val1, A_id = id1, B = val2, B_id = id2, style = "solid", arrowhead = "normal", } ) end -- forward declarations local dottify_table, dottify_userdata, dottify_thread, dottify_function local function make_label( tab, v, data, id, subid, depth ) if type( v ) == "table" then local id2 = dottify_table( v, data, depth+1 ) dottify_ref( tab, id..":"..subid, v, id2..":0", data ) return tostring( v ) elseif type( v ) == "userdata" then local id2 = dottify_userdata( v, data, depth+1 ) dottify_ref( tab, id..":"..subid, v, id2, data ) return tostring( v ) elseif type( v ) == "function" then local id2 = dottify_function( v, data, depth+1 ) dottify_ref( tab, id..":"..subid, v, id2, data ) return tostring( v ) elseif type( v ) == "thread" then local id2 = dottify_thread( v, data, depth+1 ) dottify_ref( tab, id..":"..subid, v, id2, data ) return tostring( v ) elseif type( v ) == "string" then return abbrev( v, data ) elseif type( v ) == "number" or type( v ) == "boolean" then return tostring( v ) else error( "unsupported primitive lua type" ) end end function dottify_table( tab, data, depth ) assert( type( tab ) == "table" ) update_node_depth( tab, data, depth ) if not data.node2id[ tab ] then local node = { value = tab } local id = define_node( data, node ) local label -- build label for this table if data.use_html then node.shape = "plaintext" label = [[ ]] else node.shape = "record" label = "{ <0> " .. tostring( tab ) end local handled = {} local n = 1 -- first the array part for i,v in ipairs( tab ) do local el_label = make_label( tab, v, data, id, n, depth ) if data.use_html then label = label .. [[ ]] else label = label .. " | <" .. n .. "> " .. el_label end n = n + 1 handled[ i ] = true end -- and then the hash part local keys, values = {}, {} for k,v in pairs( tab ) do node.important = true if not handled[ k ] then -- skip array part elements local k_label = make_label( tab, k, data, id, "k"..n, depth ) local v_label = make_label( tab, v, data, id, "v"..n, depth ) if data.use_html then label = label .. [[ ]] else append( keys, " " .. k_label ) append( values, " " .. v_label ) end n = n + 1 end end if data.use_html then node.label = label .. [[
]] .. tostring( tab ) .. [[
]] .. el_label .. [[
]] .. k_label .. [[ ]] .. v_label .. [[
]] else if next( keys ) ~= nil then label = label .. " | { { " .. table.concat( keys, " | " ) .. " } | { " .. table.concat( values, " | " ) .. " } }" end node.label = label .. " }" end -- and now the metatable local mt = get_metatable( tab, data.show_metatables ) if type( mt ) == "table" then local id2 = dottify_table( mt, data, depth+1 ) dottify_metatable_ref( tab, id .. ":0", mt, id2 .. ":0", data ) end end return data.node2id[ tab ] end function dottify_userdata( udata, data, depth ) assert( type( udata ) == "userdata" ) update_node_depth( udata, data, depth ) if not data.node2id[ udata ] then local id = define_node( data, { value = udata, label = tostring( udata ), shape = "box" } ) -- the metatable local mt = get_metatable( udata, data.show_metatables ) if type( mt ) == "table" then local id2 = dottify_table( mt, data, depth+1 ) dottify_metatable_ref( udata, id, mt, id2..":0", data ) end -- the environment local env = get_environment( udata, data.show_environments ) if type( env ) == "table" then local id2 = dottify_table( env, data, depth+1 ) dottify_environment_ref( udata, id, env, id2..":0", data ) end end return data.node2id[ udata ] end function dottify_thread( thread, data, depth ) assert( type( thread ) == "thread" ) update_node_depth( thread, data, depth ) if not data.node2id[ thread ] then local id = define_node( data, { value = thread, label = tostring( thread ), shape = "triangle" } ) -- the environment local env = get_environment( val, data.show_environments ) if type( env ) == "table" then local id2 = dottify_table( env, data, depth+1 ) dottify_environment_ref( thread, id, env, id2..":0", data ) end end return data.node2id[ thread ] end function dottify_function( func, data, depth ) assert( type( func ) == "function" ) update_node_depth( func, data, depth ) if not data.node2id[ func ] then local id = define_node( data, { value = func, label = tostring( func ), shape = "ellipse" } ) -- the environment local env = get_environment( func, data.show_environments ) if type( env ) == "table" then local id2 = dottify_table( env, data, depth+1 ) dottify_environment_ref( func, id, env, id2..":0", data ) end -- the upvalues if data.show_upvalues and type( debug ) == "table" and type( debug.getupvalue ) == "function" then local n = 1 repeat local name, upvalue = debug.getupvalue( func, n ) if type( upvalue ) == "table" then local id2 = dottify_table( upvalue, data, depth+1 ) dottify_upvalue_ref( func, id, upvalue, id2..":0", data, name ) elseif type( upvalue ) == "userdata" then local id2 = dottify_userdata( upvalue, data, depth+1 ) dottify_upvalue_ref( func, id, upvalue, id2, data, name ) elseif type( upvalue ) == "function" then local id2 = dottify_function( upvalue, data, depth+1 ) dottify_upvalue_ref( func, id, upvalue, id2, data, name ) elseif type( upvalue ) == "thread" then local id2 = dottify_thread( upvalue, data, depth+1 ) dottify_upvalue_ref( func, id, upvalue, id2, data, name ) end n = n + 1 until name == nil end end return data.node2id[ func ] end local option_names = { "label", "shape", "style", "dir", "arrowhead", "arrowtail", "color", "fillcolor" } local function process_options( obj ) local options = {} for _,opt in ipairs( option_names ) do if obj[ opt ] then local quote_on = "\"" local quote_off = "\"" if opt == "label" and type( obj[ opt ] ) == "string" and obj[ opt ]:match( "^<.*>$" ) then quote_on, quote_off = "<", ">" end append( options, tostring( opt ) .. "=" .. quote_on .. tostring( obj[ opt ] ) .. quote_off ) end end return options end local function write_nodes( file, data ) for _,n in ipairs( data.nodes ) do if (data.max_depth <= 0 or data.node2depth[ n.value ] <= data.max_depth) and (data.show_unimportant or n.important) then local options = process_options( n ) file:write( " ", tostring( data.node2id[ n.value ] ), " [", table.concat( options, "," ), "];\n" ) end end end local function write_edges( file, data ) for _,e in ipairs( data.edges ) do if (data.max_depth <= 0 or (data.node2depth[ e.A ] <= data.max_depth and data.node2depth[ e.B ] <= data.max_depth)) and (data.show_unimportant or (data.nodes[ data.node2id[ e.A ] ].important and data.nodes[ data.node2id[ e.B ] ].important)) then local id1 = e.A_id or data.node2id[ e.A ] local id2 = e.B_id or data.node2id[ e.B ] local options = process_options( e ) file:write( " ", tostring( id1 ), " -> ", tostring( id2 ), " [", table.concat( options, "," ), "];\n" ) end end end -- main function local function dottify( filename, val, ... ) local data = { n_nodes = 1, node2id = {}, node2depth = {}, nodes = {}, edges = {}, show_metatables = true, show_upvalues = true, show_environments = false, use_html = true, show_unimportant = false, max_depth = 0, } for i = 1, select( '#', ... ) do local opt = select( i, ... ) if opt == "noenvironments" then data.show_environments = false elseif opt == "nometatables" then data.show_metatables = false elseif opt == "noupvalues" then data.show_upvalues = false elseif opt == "nohtml" then data.use_html = false elseif opt == "environments" then data.show_environments = true elseif opt == "metatables" then data.show_metatables = true elseif opt == "upvalues" then data.show_upvalues = true elseif opt == "html" then data.use_html = true elseif opt == "unimportant" then data.show_unimportant = true elseif type( opt ) == "number" then data.max_depth = opt end end local t = type( val ) if t == "table" then local id = dottify_table( val, data, 1 ) data.nodes[ id ].important = true elseif t == "function" then local id = dottify_function( val, data, 1 ) data.nodes[ id ].important = true elseif t == "thread" then local id = dottify_thread( val, data, 1 ) data.nodes[ id ].important = true elseif t == "userdata" then local id = dottify_userdata( val, data, 1 ) data.nodes[ id ].important = true else io.stderr:write( "warning: unsuitable value for dotlua!\n" ) end local file = assert( io.open( filename, "w" ) ) file:write( "digraph {\n" ) write_nodes( file, data ) write_edges( file, data ) file:write( "}\n" ) file:close() end return dottify