├── doc ├── example1.gif ├── example2.gif ├── example3.gif ├── example4.gif ├── example5.gif ├── example6.gif └── readme.txt ├── README.md ├── tests ├── light.c ├── microscope8.test.lua ├── microscope4.test.lua ├── bytecode.lua ├── microscope5.test.lua ├── microscope3.test.lua ├── microscope2.test.lua ├── microscope6.test.lua ├── microscope7.test.lua ├── HISTO ├── newproxy.c └── microscope1.test.lua ├── microscope-scm-0.rockspec └── src └── microscope.lua /doc/example1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siffiejoe/lua-microscope/HEAD/doc/example1.gif -------------------------------------------------------------------------------- /doc/example2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siffiejoe/lua-microscope/HEAD/doc/example2.gif -------------------------------------------------------------------------------- /doc/example3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siffiejoe/lua-microscope/HEAD/doc/example3.gif -------------------------------------------------------------------------------- /doc/example4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siffiejoe/lua-microscope/HEAD/doc/example4.gif -------------------------------------------------------------------------------- /doc/example5.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siffiejoe/lua-microscope/HEAD/doc/example5.gif -------------------------------------------------------------------------------- /doc/example6.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siffiejoe/lua-microscope/HEAD/doc/example6.gif -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | lua-microscope 2 | ============== 3 | 4 | Creates images of arbitrary Lua values using GraphViz 5 | 6 | See [here](http://siffiejoe.github.io/lua-microscope/). 7 | 8 | -------------------------------------------------------------------------------- /tests/light.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | int luaopen_light( lua_State* L ) { 6 | lua_pushlightuserdata( L, NULL ); 7 | return 1; 8 | } 9 | 10 | -------------------------------------------------------------------------------- /tests/microscope8.test.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/lua 2 | 3 | package.path = [[../src/?.lua;]] .. package.path 4 | local todot = require( "microscope" ) 5 | 6 | todot( "example9.dot", package, "environments", todot, _G ) 7 | 8 | -------------------------------------------------------------------------------- /tests/microscope4.test.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/lua 2 | 3 | package.path = [[../src/?.lua;]] .. package.path 4 | 5 | local t = { { { { { {} } } } } } 6 | 7 | local todot = require( "microscope" ) 8 | todot( "example3.dot", t ) 9 | todot( "example4.dot", t, 3 ) 10 | 11 | -------------------------------------------------------------------------------- /tests/bytecode.lua: -------------------------------------------------------------------------------- 1 | local up1 = false 2 | local up2 = newproxy( true ) 3 | setfenv( up2, {} ) 4 | local t1 = { val = 1 } 5 | local t2 = { val = 2 } 6 | setmetatable( t1, { __index = function( t, k ) 7 | if t2[ k ] ~= nil then 8 | return t2[ k ] 9 | else 10 | return up1 or up2 or print 11 | end 12 | end } ) 13 | setmetatable( t2, { __index = t1 } ) 14 | return t1 15 | 16 | -------------------------------------------------------------------------------- /tests/microscope5.test.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/lua 2 | 3 | package.path = [[../src/?.lua;]] .. package.path 4 | 5 | local function f1() end 6 | local function f2() 7 | return f2() 8 | end 9 | local t1 = { func1 = f1, func2 = f2 } 10 | local t2 = { func1 = f1 } 11 | local t = { t1, t2, {} } 12 | 13 | local todot = require( "microscope" ) 14 | todot( "example5.dot", t ) 15 | todot( "example6.dot", t, "leaves" ) 16 | 17 | -------------------------------------------------------------------------------- /tests/microscope3.test.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/lua 2 | 3 | package.path = [[../src/?.lua;]] .. package.path 4 | local ms = require( "microscope" ) 5 | 6 | local t1 = { val = 1 } 7 | local t2 = { 1, 2, 3, val = 2 } 8 | setmetatable( t1, { __index = t2 } ) 9 | _ENV = t1 10 | local function f1() 11 | print( val, t2.val ) 12 | end 13 | if _VERSION == "Lua 5.1" then 14 | setfenv( f1, t1 ) 15 | end 16 | 17 | ms( "example2.dot", f1, "environments" ) 18 | 19 | -------------------------------------------------------------------------------- /tests/microscope2.test.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/lua 2 | 3 | package.path = [[../src/?.lua;]] .. package.path 4 | 5 | local up1 = false 6 | local up2 = io.stdout 7 | local t1 = { val = 1 } 8 | local t2 = { val = 2 } 9 | setmetatable( t1, { __index = function( t, k ) 10 | if t2[ k ] ~= nil then 11 | return t2[ k ] 12 | else 13 | return up1 or up2 14 | end 15 | end } ) 16 | setmetatable( t2, { __index = t1 } ) 17 | 18 | require( "microscope" )( "example1.dot", t1 ) 19 | 20 | -------------------------------------------------------------------------------- /tests/microscope6.test.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/lua 2 | 3 | package.path = [[../src/?.lua;]] .. package.path 4 | 5 | local function f1() end 6 | local function f2() 7 | return f2() 8 | end 9 | local t1 = { func1 = f1, func2 = f2 } 10 | setmetatable( t1, { __metatable = false } ) 11 | local t2 = { func1 = f1, tab1 = t1, file = io.stdout } 12 | setmetatable( t2, { 13 | __index = t1, 14 | __metatable = f2 15 | } ) 16 | 17 | require( "microscope" )( "example7.dot", t2, "environments", 3 ) 18 | 19 | -------------------------------------------------------------------------------- /tests/microscope7.test.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/lua 2 | 3 | package.path = [[../src/?.lua;]] .. package.path 4 | 5 | local function f1() end 6 | local function f2() 7 | return f2() 8 | end 9 | local t1 = { func1 = f1, func2 = f2 } 10 | setmetatable( t1, { __metatable = false } ) 11 | local t2 = { func1 = f1, tab1 = t1, file = io.stdout } 12 | setmetatable( t2, { 13 | __index = t1, 14 | __metatable = f2 15 | } ) 16 | 17 | debug = nil -- disable debug module 18 | package.preload.debug = nil 19 | package.loaded.debug = nil 20 | require( "microscope" )( "example8.dot", t2, "environments", 3 ) 21 | 22 | -------------------------------------------------------------------------------- /microscope-scm-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "microscope" 2 | version = "scm-0" 3 | source = { 4 | url = "git://github.com/siffiejoe/lua-microscope.git", 5 | } 6 | description = { 7 | summary = "Creates images of arbitrary Lua values using GraphViz", 8 | detailed = [[ 9 | This Lua module dumps arbitrarily complex Lua datastructures as 10 | GraphViz .dot-files that can be transformed into a variety of 11 | image formats. 12 | ]], 13 | homepage = "http://siffiejoe.github.io/lua-microscope/", 14 | license = "MIT" 15 | } 16 | dependencies = { 17 | "lua >= 5.1, < 5.5" 18 | } 19 | build = { 20 | type = "builtin", 21 | modules = { 22 | microscope = "src/microscope.lua" 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /tests/HISTO: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | LUAV=jit 5 | LUAV=5.1 6 | LUAV=5.2 7 | LUAV=5.3 8 | LUAV=5.4 9 | 10 | compile_lua() { 11 | luac$LUAV -o "$2" "$1" 12 | } 13 | compile_lua_s() { 14 | luac$LUAV -s -o "$2" "$1" 15 | } 16 | if [ "$LUAV" == 5.1 ]; then 17 | INC=/usr/include/lua5.1 18 | elif [ "$LUAV" == 5.2 ]; then 19 | INC=/usr/include/lua5.2 20 | elif [ "$LUAV" == 5.3 ]; then 21 | INC=/usr/include/lua5.2 22 | elif [ "$LUAV" == 5.4 ]; then 23 | INC=/home/siffiejoe/.self/programs/lua5.4 24 | elif [ "$LUAV" == jit ]; then 25 | INC=/home/siffiejoe/.self/programs/luajit-2.0/include/luajit-2.0 26 | compile_lua() { 27 | luajit -bg "$1" "$2" 28 | } 29 | compile_lua_s() { 30 | luajit -bs "$1" "$2" 31 | } 32 | else 33 | echo "invalid Lua version: $LUAV!" 1>&2 34 | exit 1 35 | fi 36 | gcc -Wall -Wextra -fpic -I"$INC" -shared -o light.so light.c 37 | gcc -Wall -Wextra -fpic -I"$INC" -shared -o newproxy.so newproxy.c 38 | compile_lua bytecode.lua bytecode.n.luac 39 | compile_lua_s bytecode.lua bytecode.s.luac 40 | 41 | for i in `seq 1 8`; do 42 | echo "running microscope$i.test.lua" 43 | lua$LUAV microscope$i.test.lua 44 | done 45 | 46 | for f in *.dot; do 47 | if [ -f "$f" ]; then 48 | img="${f%.dot}.gif" 49 | echo "making $img..." 50 | dot -T gif -o "$img" "$f" 51 | fi 52 | done 53 | 54 | exit 0 55 | 56 | rm -f *.dot *.gif *.luac *.so 57 | -------------------------------------------------------------------------------- /tests/newproxy.c: -------------------------------------------------------------------------------- 1 | /***** taken almost literally from Lua 5.1.4 source, so: ********************** 2 | * Copyright (C) 1994-2008 Lua.org, PUC-Rio. All rights reserved. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining 5 | * a copy of this software and associated documentation files (the 6 | * "Software"), to deal in the Software without restriction, including 7 | * without limitation the rights to use, copy, modify, merge, publish, 8 | * distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so, subject to 10 | * the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be 13 | * included in all copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 19 | * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 20 | * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 21 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | ******************************************************************************/ 23 | 24 | 25 | #include 26 | #include 27 | #include 28 | 29 | 30 | static int newproxy( lua_State* L ) { 31 | lua_settop( L, 1 ); 32 | #if LUA_VERSION_NUM >= 504 33 | lua_newuserdatauv( L, 0, 2 ); 34 | #else 35 | lua_newuserdata( L, 0 ); 36 | #endif 37 | if( lua_toboolean( L, 1 ) == 0 ) /* false value */ 38 | return 1; 39 | else if( lua_isboolean( L, 1 ) ) { /* true */ 40 | lua_newtable( L ); 41 | lua_pushvalue( L, -1 ); 42 | lua_pushboolean( L, 1 ); 43 | lua_rawset( L, lua_upvalueindex( 1 ) ); 44 | } else { /* rest (hopefully proxy) */ 45 | int validproxy = 0; 46 | if( lua_getmetatable( L, 1 ) ) { 47 | lua_rawget( L, lua_upvalueindex( 1 ) ); 48 | validproxy = lua_toboolean( L, -1 ); 49 | lua_pop( L, 1 ); 50 | } 51 | luaL_argcheck( L, validproxy, 1, "boolean or proxy expected" ); 52 | lua_getmetatable( L, 1 ); 53 | } 54 | lua_setmetatable( L, 2 ); 55 | return 1; 56 | } 57 | 58 | 59 | LUALIB_API int luaopen_newproxy( lua_State* L ) { 60 | lua_createtable( L, 0, 1 ); 61 | lua_pushvalue( L, -1 ); 62 | lua_setmetatable( L, -2 ); 63 | lua_pushliteral( L, "kv" ); 64 | lua_setfield( L, -2, "__mode" ); 65 | lua_pushcclosure( L, newproxy, 1 ); 66 | return 1; 67 | } 68 | 69 | -------------------------------------------------------------------------------- /doc/readme.txt: -------------------------------------------------------------------------------- 1 | # microscope -- Visualizing Complex Lua Values Using GraphViz # 2 | 3 | ## Introduction ## 4 | 5 | Checking the value of a Lua variable during debugging is often 6 | invaluable and `print()` is a commonly used debugging tool. But what 7 | about complex table values? Many Lua programmers have written their 8 | own pretty-printer or data dumper and some even use it for 9 | (de-)serializing Lua data structures. But this seems to reach its 10 | limit when one takes cyclic references, metatables, upvalues and 11 | environment tables into account. 12 | 13 | This module dumps arbitrarily complex Lua values as [GraphViz][1] 14 | .dot-files that can be transformed into a variety of image formats. 15 | 16 | [1]: http://www.graphviz.org/ (GraphViz Homepage) 17 | 18 | 19 | ## Basic Usage ## 20 | 21 | Somewhere in your Lua scripts require this module and call the result, 22 | passing a file name and the value to be inspected: 23 | 24 | $ cat > test.lua 25 | local up1 = false 26 | local up2 = io.stdout 27 | local t1 = { val = 1 } 28 | local t2 = { val = 2 } 29 | setmetatable( t1, { __index = function( t, k ) 30 | if t2[ k ] ~= nil then 31 | return t2[ k ] 32 | else 33 | return up1 or up2 34 | end 35 | end } ) 36 | setmetatable( t2, { __index = t1 } ) 37 | 38 | require( "microscope" )( "example1.dot", t1 ) 39 | ^D 40 | 41 | Run the script and convert the resulting .dot file to a nice image: 42 | 43 | $ lua test.lua 44 | $ dot -T gif -o example1.gif example1.dot 45 | 46 | This results in: 47 | 48 | ![example1](example1.gif) 49 | 50 | 51 | ## Reference ## 52 | 53 | The function returned from the `require`-call takes two mandatory 54 | arguments: 55 | 56 | 1. a file name (typically a .dot file) or an output function 57 | 2. a Lua value to show 58 | 59 | After those two arguments, a number of optional arguments can be given 60 | (in any order): 61 | 62 | * a number 63 | 64 | This limits the number of linked values to display. `1` means 65 | only the passed Lua value itself, `2` means the value itself and 66 | any value that can be reached in 1 step, and so on. Default is `0` 67 | which means to follow all links. 68 | 69 | * any table, userdata, function or thread 70 | 71 | Prunes the resulting graph at the given value. Typical use cases 72 | are the `microscope` function itself, `package.loaded` or even 73 | `_G`. 74 | 75 | * `"html"`, `"nohtml"` 76 | 77 | Enables/disables the generation of HTML code in the .dot files. 78 | HTML code is used for prettier tables, but older GraphViz versions 79 | cannot handle HTML. If you see strange code in your images, try 80 | the `"nohtml"`-option. Default is `"html"`. 81 | 82 | * `"environments"`, `"noenvironments"` 83 | 84 | Enables/disables display of environment tables. Most functions 85 | share the global environment and the pictures get quite big, so 86 | the default is `"noenvironments"`. 87 | 88 | * `"upvalues"`, `"noupvalues"` 89 | 90 | Enables/disables display of upvalues for functions. Default is 91 | `"upvalues"`. 92 | 93 | * `"metatables"`, `"nometatables"` 94 | 95 | Enables/disables display of metatables for tables/userdata. 96 | Default is `"metatables"`. 97 | 98 | * `"leaves"`, `"noleaves"` 99 | 100 | Sometimes (when a value is part of a table and does not have any 101 | outgoing links), it is unnecessary to draw an extra shape for this 102 | value, as it would clutter the image. This option enables/disables 103 | the generation of leaf nodes in the graph. Default is 104 | `"noleaves"`. 105 | 106 | * `"locals"`, `"nolocals"` 107 | 108 | Enables/disables display of a table-like stack with local 109 | variables for all suspended and active coroutines. If no coroutine 110 | is active, the main stack is displayed as well. Default is 111 | `"nolocals"`. All settings that limit graph node output also apply 112 | to the references in the stack(s). 113 | 114 | * `"registry"`, `"noregistry"` 115 | 116 | Enables/disables display of the Lua registry table. The registry 117 | is included as another root node in the graph output. Default is 118 | `"noregistry"`. All settings that limit graph node output also 119 | apply to the references in the registry. 120 | 121 | * `"sizes"`, `"nosizes"` 122 | 123 | If [lua-getsize][2] is available in `package.cpath`, the size of a 124 | table, userdata, thread, and function will be added to the label 125 | automatically. This setting enables/disables the creation of extra 126 | nodes in the graph also showing the object's size. The default is 127 | `"nosizes"`. 128 | 129 | * any other string 130 | 131 | Any other string is used as a label for the graph. If multiple 132 | labels are given, only the last one is used. 133 | 134 | The output function (if used as first argument) takes one argument and 135 | should write its argument and a trailing newline to wherever you want 136 | (the `print`-function is compatible). 137 | 138 | [2]: http://code.matthewwild.co.uk/lua-getsize/ 139 | 140 | 141 | ### Examples ### 142 | 143 | Showing (custom) environment tables: 144 | 145 | local t1 = { val = 1 } 146 | local t2 = { 1, 2, 3, val = 2 } 147 | setmetatable( t1, { __index = t2 } ) 148 | local function f1() 149 | print( val, t2.val ) 150 | end 151 | setfenv( f1, t1 ) 152 | 153 | require( "microscope" )( "example2.dot", f1, "environments" ) 154 | 155 | Leads to: 156 | 157 | ![example2](example2.gif) 158 | 159 | Limiting displayed elements: 160 | 161 | local t = { { { { { {} } } } } } 162 | 163 | local todot = require( "microscope" ) 164 | todot( "example3.dot", t ) 165 | todot( "example4.dot", t, 3 ) 166 | 167 | Leads to: 168 | 169 | ![example3](example3.gif) and ![example4](example4.gif) 170 | 171 | Showing leaf nodes in the graph: 172 | 173 | local function f1() end 174 | local function f2() 175 | return f2() 176 | end 177 | local t1 = { func1 = f1, func2 = f2 } 178 | local t2 = { func1 = f1 } 179 | local t = { t1, t2, {} } 180 | 181 | local todot = require( "microscope" ) 182 | todot( "example5.dot", t ) 183 | todot( "example6.dot", t, "leaves" ) 184 | 185 | Leads to: 186 | 187 | ![example5](example5.gif) and ![example6](example6.gif) 188 | 189 | 190 | ## Download ## 191 | 192 | The source code (with documentation and test scripts) is available on 193 | [github][3]. 194 | 195 | [3]: https://github.com/siffiejoe/lua-microscope/ 196 | 197 | 198 | ## Installation ## 199 | 200 | There are two ways to install this module, either using luarocks (if 201 | this module already is in the [main luarocks repository][4]) or 202 | manually. 203 | 204 | Using luarocks, simply type: 205 | 206 | luarocks install microscope 207 | 208 | To install the module manually just drop the Lua file `microscope.lua` 209 | somewhere into your Lua `package.path`. 210 | 211 | [4]: http://luarocks.org/repositories/rocks/ (Main Repository) 212 | 213 | 214 | ## Changes ## 215 | 216 | Version 0.5: 217 | 218 | * Compatibility with Lua 5.4 219 | 220 | Version 0.4: 221 | 222 | * Compatibility with Lua 5.3 223 | * Show objects' sizes if [lua-getsize][2] is available. 224 | * Protect from errors in `__tostring` metamethods 225 | 226 | Version 0.3: 227 | 228 | * Fixed long labels from `__tostring` metamethods. 229 | * Support for LuaJIT's `cdata` values. 230 | 231 | Version 0.2: 232 | 233 | * First public release. 234 | 235 | 236 | ## Contact ## 237 | 238 | Philipp Janda, siffiejoe(a)gmx.net 239 | 240 | Comments and feedback are always welcome. 241 | 242 | 243 | ## License ## 244 | 245 | `microscope` is *copyrighted free software* distributed under the MIT 246 | license (the same license as Lua 5.1). The full license text follows: 247 | 248 | microscope (c) 2013-2020 Philipp Janda 249 | 250 | Permission is hereby granted, free of charge, to any person obtaining 251 | a copy of this software and associated documentation files (the 252 | "Software"), to deal in the Software without restriction, including 253 | without limitation the rights to use, copy, modify, merge, publish, 254 | distribute, sublicense, and/or sell copies of the Software, and to 255 | permit persons to whom the Software is furnished to do so, subject to 256 | the following conditions: 257 | 258 | The above copyright notice and this permission notice shall be 259 | included in all copies or substantial portions of the Software. 260 | 261 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 262 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 263 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 264 | IN NO EVENT SHALL THE AUTHOR OR COPYRIGHT HOLDER BE LIABLE FOR ANY 265 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 266 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 267 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 268 | 269 | 270 | -------------------------------------------------------------------------------- /tests/microscope1.test.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/lua 2 | 3 | package.path = [[../src/?.lua;]] .. package.path 4 | local newproxy = newproxy or require( "newproxy" ) 5 | local microscope = require( "microscope" ) 6 | local light = require( "light" ) 7 | local full = newproxy() 8 | local func = function() end 9 | local co = coroutine.create( function() end ) 10 | local cdata, ctype 11 | do 12 | local ok, ffi = pcall( require, "ffi" ) 13 | if ok then 14 | ctype = ffi.metatype( "struct { int number; }", {} ) 15 | cdata = ffi.new( ctype ) 16 | end 17 | end 18 | 19 | local n = 0 20 | local function dot( v, label, ... ) 21 | local fname = string.format( "test%02d.dot", n ) 22 | n = n + 1 23 | print( "", fname, tostring( v ) ) 24 | microscope( fname, v, label, ... ) 25 | end 26 | 27 | 28 | print( "testing all Lua types as direct arguments ..." ) 29 | dot( nil, "single nil value" ) 30 | dot( true, "single boolean value" ) 31 | dot( 123, "single number value" ) 32 | dot( "hello world", "single string value" ) 33 | dot( func, "plain function" ) 34 | dot( light, "plain light userdata value" ) 35 | dot( full, "plain full userdata value" ) 36 | dot( co, "plain coroutine/thread value" ) 37 | dot( {}, "plain empty table value" ) 38 | if ctype and cdata then 39 | dot( ctype, "luajit ctype object" ) 40 | dot( cdata, "luajit cdata object" ) 41 | end 42 | 43 | print( "testing all Lua types as table keys/values ..." ) 44 | do 45 | local t = { 46 | [ true ] = false, 47 | [ 123 ] = 456, 48 | xyz = "abc", 49 | [ func ] = func, 50 | [ light ] = light, 51 | [ full ] = full, 52 | [ co ] = co, 53 | [ {} ] = {} 54 | } 55 | if ctype and cdata then 56 | t[ ctype ] = ctype 57 | t[ cdata ] = cdata 58 | end 59 | dot( t, "all Lua types as table keys and values" ) 60 | end 61 | 62 | print( "testing all Lua types as upvalues ..." ) 63 | do 64 | local _ = nil 65 | local a = true 66 | local b = 123 67 | local c = "hello world" 68 | local d = func 69 | local e = light 70 | local f = full 71 | local g = co 72 | local h = {} 73 | local i = ctype 74 | local j = cdata 75 | dot( function() 76 | return _, a, b, c, d, e, f, g, h, i, j 77 | end, "all Lua types as function upvalues" ) 78 | end 79 | 80 | print( "testing all Lua types as environments ..." ) 81 | do 82 | local function makeuenv( ... ) 83 | local u = newproxy() 84 | if _VERSION == "Lua 5.1" then 85 | debug.setfenv( u, (...) ) 86 | elseif _VERSION < "Lua 5.4" then 87 | debug.setuservalue( u, (...) ) 88 | else 89 | for i = 1, select( '#', ... ) do 90 | debug.setuservalue( u, select( i, ... ), i ) 91 | end 92 | end 93 | return u 94 | end 95 | local function makefenv( val ) 96 | local _VERSION = _VERSION 97 | local _ENV = val 98 | local function f() print( "bla" ) end 99 | if _VERSION == "Lua 5.1" then 100 | setfenv( f, _ENV ) 101 | end 102 | return f 103 | end 104 | local function maketenv( val ) 105 | if _VERSION == "Lua 5.1" then 106 | local c = coroutine.create( func ) 107 | debug.setfenv( c, val ) 108 | return c 109 | end 110 | end 111 | local function makefenvs() 112 | if _VERSION == "Lua 5.1" then 113 | return makefenv( {} ) 114 | else 115 | return makefenv( nil ), makefenv( true ), makefenv( 123 ), 116 | makefenv( "hello" ), makefenv( func ), makefenv( light ), 117 | makefenv( full ), makefenv( co ), makefenv( {}, "hello" ) 118 | end 119 | end 120 | local function makeuenvs() 121 | if _VERSION < "Lua 5.3" then 122 | return makeuenv( {} ) 123 | else 124 | return makeuenv( nil ), makeuenv( true ), makeuenv( 123 ), 125 | makeuenv( "hello" ), makeuenv( func ), makeuenv( light ), 126 | makeuenv( full ), makeuenv( co ), makeuenv( {}, 456 ) 127 | end 128 | end 129 | local t = { 130 | { makeuenvs() }, { makefenvs() }, maketenv{} 131 | } 132 | dot( t, "all possible Lua types as environments", "environments" ) 133 | end 134 | 135 | print( "testing escapes in string (and (no-)html option) ..." ) 136 | do 137 | local t = { "'N'n\\n\n'G\\G'l\\l" } 138 | for i = 0, 63 do 139 | local s = string.char( i*4, i*4+1, i*4+2, i*4+3 ) 140 | if i < 32 then 141 | t[ #t+1 ] = s 142 | else 143 | t[ s ] = s 144 | end 145 | end 146 | dot( t, "all characters in labels (HTML version)", "html" ) 147 | dot( t, "all characters in labels (plain version)", "nohtml" ) 148 | end 149 | 150 | print( "testing abbreviation of long string ..." ) 151 | dot( "This is a very long string that should be cut off somewhere!", 152 | "string which is abbreviated as a label" ) 153 | 154 | print( "testing function with all features ..." ) 155 | do 156 | local f, env 157 | do 158 | env = {} 159 | local _ENV = env 160 | local a = nil 161 | local b = 123 162 | function f() return a or b or print end 163 | end 164 | if _VERSION == "Lua 5.1" then 165 | setfenv( f, env ) 166 | end 167 | dot( f, "function with all features (upvalues+env)", "environments" ) 168 | end 169 | 170 | print( "testing udata with all features ..." ) 171 | do 172 | local u = newproxy( true ) 173 | if _VERSION == "Lua 5.1" then 174 | debug.setfenv( u, {} ) 175 | else 176 | debug.setuservalue( u, {} ) 177 | end 178 | dot( u, "userdata with all features (metatable+env)", "environments" ) 179 | end 180 | 181 | print( "testing table with all features ..." ) 182 | do 183 | local function dummy() end 184 | local function len() return 0 end 185 | local t = { 186 | 1, "2", 3, 187 | key1 = "val1", 188 | key2 = true 189 | } 190 | setmetatable( t, { __index = t, __ipairs = dummy, __pairs = dummy, 191 | __len = len } ) 192 | dot( t, "table with all features (array+hash part,metatable)" ) 193 | dot( { 1, 2, 3, nil, 4, 5, 6, nil, nil, nil, 8, nil, 9, nil, nil, 10 }, 194 | "array (table) with holes at positions 4,8,9,10,12,14,15" ) 195 | end 196 | 197 | print( "testing thread with all features ..." ) 198 | do 199 | local c = coroutine.create( func ) 200 | if _VERSION == "Lua 5.1" then 201 | debug.setfenv( c, {} ) 202 | end 203 | dot( c, "thread with all features (env in Lua 5.1)", "environments" ) 204 | end 205 | 206 | print( "testing shared upvalues ..." ) 207 | do 208 | local value = 1 209 | local reference = {} 210 | local distval = 2 211 | local function f1() 212 | return value, reference, distval 213 | end 214 | local distval = 2 215 | local function f2() 216 | return value, reference, distval 217 | end 218 | local t = { 219 | f1, f2 220 | } 221 | dot( t, "shared upvalues (in Lua5.1 only for reference types)" ) 222 | end 223 | 224 | print( 'testing "(no-)environments" option ...' ) 225 | do 226 | local f 227 | do 228 | local _ENV = {} 229 | function f() 230 | print( "hello world" ) 231 | end 232 | if _VERSION == "Lua 5.1" then 233 | setfenv( f, _ENV ) 234 | end 235 | end 236 | local u, th = newproxy(), coroutine.create( f ) 237 | if _VERSION == "Lua 5.1" then 238 | debug.setfenv( u, {} ) 239 | debug.setfenv( th, {} ) 240 | else 241 | debug.setuservalue( u, {} ) 242 | end 243 | local t = { u, f, th } 244 | dot( t, "with environments", "environments" ) 245 | dot( t, "without environments", "noenvironments" ) 246 | end 247 | 248 | print( 'testing "(no-)leaves" option ...' ) 249 | do 250 | local t1 = {} 251 | local t2 = {} 252 | local t3 = {} 253 | local t4 = { t2 } 254 | setmetatable( t3, {} ) 255 | local u1 = newproxy() 256 | local u2 = newproxy() 257 | if _VERSION == "Lua 5.1" then 258 | debug.setfenv( u2, {} ) 259 | else 260 | debug.setuservalue( u2, {} ) 261 | end 262 | local a, b = true, false 263 | local function f1() return true, false end 264 | local function f2() return a, b end 265 | local t = { t1, t2, t3, t4, u1, u2, f1, f2 } 266 | dot( t, "with leaf nodes (env, pruned at _G)", "leaves", "environments", _G ) 267 | dot( t, "without leaf nodes (env, pruned at _G)", "noleaves", "environments", _G ) 268 | end 269 | 270 | print( 'testing "(no-)upvalues" option ...' ) 271 | do 272 | local a, b, c, d, e = nil, true, 123, "hello", {} 273 | local function f() 274 | return a, b, c, d, e 275 | end 276 | dot( f, "with upvalues", "upvalues" ) 277 | dot( f, "without upvalues", "noupvalues" ) 278 | end 279 | 280 | print( 'testing "(no-)metatables" option ...' ) 281 | do 282 | local t1 = {} 283 | setmetatable( t1, {} ) 284 | local t2 = { io.stdout, t1 } 285 | dot( t2, "with metatables", "metatables" ) 286 | dot( t2, "without metatables", "nometatables" ) 287 | end 288 | 289 | print( 'testing "(no-)sizes" option ...' ) 290 | do 291 | local t1 = {} 292 | setmetatable( t1, {} ) 293 | local function f() 294 | return t1, func, full, co 295 | end 296 | dot( f, "with sizes", "sizes" ) 297 | dot( f, "without sizes", "nosizes" ) 298 | end 299 | 300 | print( "testing max_depth ..." ) 301 | do 302 | local t1 = { 1 } 303 | local t2 = { { t1, { t1, { {} } } } } 304 | dot( t2, "nested tables, unlimited depth" ) 305 | dot( t2, "nested tables, depth limited to 3", 3 ) 306 | end 307 | 308 | print( "testing pruning ..." ) 309 | do 310 | local t0 = { 0 } 311 | local t1 = { t0 } 312 | local t2 = { t0, t1 } 313 | dot( t2, "nested tables without pruning" ) 314 | dot( t2, "nested tables pruned at " .. tostring( t1 ), t1 ) 315 | end 316 | 317 | print( "testing registry ..." ) 318 | dot( "dummy", "registry table with max_depth 2", "registry", 2 ) 319 | 320 | print( "testing locals and (no-)html option ..." ) 321 | do 322 | dot( "dummy", "locals from main thread", "locals", 3, microscope ) 323 | 324 | local function f1( arg1 ) 325 | dot( "dummy", "locals from main active coroutine", 326 | "locals", 3, microscope ) 327 | coroutine.yield( arg1 ) 328 | end 329 | 330 | local c1 331 | 332 | local function f2( ... ) 333 | local a = { 334 | bool = true, 335 | num = 1.234, 336 | func = f1, 337 | co = c1, 338 | } 339 | a.a = a 340 | return f1( a ) 341 | end 342 | 343 | local function f3( v, n ) 344 | for i = 1, n do 345 | dot( v, "locals with max_depth 3 (html version)", 346 | "locals", 3, microscope ) 347 | dot( v, "locals with max_depth 3 (nohtml version)", 348 | "locals", "nohtml", 3, microscope ) 349 | end 350 | end 351 | 352 | c1 = coroutine.create( f2 ) 353 | local f4 = coroutine.wrap( f3 ) 354 | coroutine.resume( c1, 1, 2, 3 ) 355 | f4( c1, 1 ) 356 | end 357 | 358 | print( "testing stripped bytecode ..." ) 359 | do 360 | local stripped, nonstripped 361 | local env = { setmetatable = setmetatable, newproxy = newproxy } 362 | if _VERSION == "Lua 5.1" then 363 | nonstripped = assert( loadfile( "bytecode.n.luac" ) ) 364 | stripped = assert( loadfile( "bytecode.s.luac" ) ) 365 | env.setfenv = debug.setfenv 366 | setfenv( nonstripped, env ) 367 | setfenv( stripped, env ) 368 | else 369 | env.setfenv = debug.setuservalue 370 | nonstripped = assert( loadfile( "bytecode.n.luac", "b", env ) ) 371 | stripped = assert( loadfile( "bytecode.s.luac", "b", env ) ) 372 | end 373 | dot( nonstripped(), "using unstripped bytecode", 374 | "environments", env.setmetatable, env.newproxy, env.setfenv ) 375 | dot( stripped(), "using stripped bytecode", 376 | "environments", env.setmetatable, env.newproxy, env.setfenv ) 377 | end 378 | 379 | print( "testing without debug table ..." ) 380 | do 381 | local u = newproxy( true ) 382 | if _VERSION == "Lua 5.1" then 383 | debug.setfenv( u, {} ) 384 | else 385 | debug.setuservalue( u, {} ) 386 | end 387 | local mt, f = getmetatable( u ) 388 | do 389 | local _ENV = {} 390 | function f() return u, mt, print end 391 | if _VERSION == "Lua 5.1" then 392 | setfenv( f, _ENV ) 393 | end 394 | end 395 | mt.__metatable = f 396 | local t = { u, mt, f } 397 | dot( t, "with debug module available", "environments" ) 398 | local olddebug, olddebugpre = debug, package.preload.debug 399 | local oldmicroscope = microscope 400 | package.loaded.debug = nil 401 | package.preload.debug = nil 402 | package.loaded.microscope = nil 403 | debug = nil 404 | microscope = require( "microscope" ) 405 | dot( t, "only get(metatable|fenv) available", "environments" ) 406 | debug = olddebug 407 | package.loaded.debug = olddebug 408 | package.preload.debug = olddebugpre 409 | package.loaded.microscope = oldmicroscope 410 | microscope = oldmicroscope 411 | end 412 | 413 | print( "testing without debug and certain baselib functions ..." ) 414 | do 415 | local u = newproxy( true ) 416 | if _VERSION == "Lua 5.1" then 417 | debug.setfenv( u, {} ) 418 | else 419 | debug.setuservalue( u, {} ) 420 | end 421 | local mt, f = getmetatable( u ) 422 | do 423 | local _ENV = {} 424 | function f() return u, mt, print end 425 | if _VERSION == "Lua 5.1" then 426 | setfenv( f, _ENV ) 427 | end 428 | end 429 | mt.__metatable = f 430 | local t = { u, mt, f } 431 | dot( t, "with debug module available", "environments" ) 432 | local olddebug, olddebugpre = debug, package.preload.debug 433 | local oldmicroscope = microscope 434 | local ogetfenv, ogetmetatable = getfenv, getmetatable 435 | getfenv, getmetatable = nil, nil 436 | package.loaded.debug = nil 437 | package.preload.debug = nil 438 | package.loaded.microscope = nil 439 | debug = nil 440 | microscope = require( "microscope" ) 441 | dot( t, "without debug module and get(metatable|fenv)", "environments" ) 442 | getfenv, getmetatable = ogetfenv, ogetmetatable 443 | debug = olddebug 444 | package.loaded.debug = olddebug 445 | package.preload.debug = olddebugpre 446 | package.loaded.microscope = oldmicroscope 447 | microscope = oldmicroscope 448 | end 449 | 450 | print( "testing alternative output function ..." ) 451 | microscope( function( s ) print( ">", s ) end, nil ) 452 | 453 | print( "testing for old bugs ..." ) 454 | do 455 | local t = {} 456 | setmetatable( t, { 457 | __tostring = function( v ) 458 | return "table: abcdefghijklmnopqrstuvwxyz0123456789" 459 | end 460 | } ) 461 | local u = newproxy( true ) 462 | getmetatable( u ).__tostring = function( u ) 463 | return "udata: abcdefghijklmnopqrstuvwxyz0123456789" 464 | end 465 | dot( { t, u }, "abbreviation in table cells" ) 466 | end 467 | 468 | do 469 | local t = {} 470 | local mt = { __tostring = function() error( "Argh!" ) end } 471 | setmetatable( t, mt ) 472 | local u = newproxy( true ) 473 | getmetatable( u ).__tostring = function( u ) 474 | error( "Argh!" ) 475 | end 476 | debug.setmetatable( 1, mt ) 477 | debug.setmetatable( true, mt ) 478 | dot( { t, u, false, 123 }, "__tostring raising error" ) 479 | mt.__tostring = nil 480 | end 481 | 482 | -- TODO ;-) 483 | 484 | -------------------------------------------------------------------------------- /src/microscope.lua: -------------------------------------------------------------------------------- 1 | -- generate a graphviz graph from a lua table structure 2 | 3 | local max_label_length = 25 4 | 5 | -- cache globals 6 | local assert = assert 7 | local require = assert( require ) 8 | local _VERSION = assert( _VERSION ) 9 | local type = assert( type ) 10 | local tostring = assert( tostring ) 11 | local select = assert( select ) 12 | local next = assert( next ) 13 | local rawget = assert( rawget ) 14 | local rawset = assert( rawset ) 15 | local pcall = assert( pcall ) 16 | local string = require( "string" ) 17 | local ssub = assert( string.sub ) 18 | local sgsub = assert( string.gsub ) 19 | local sformat = assert( string.format ) 20 | local sbyte = assert( string.byte ) 21 | local table = require( "table" ) 22 | local tconcat = assert( table.concat ) 23 | -- optional ... 24 | local getmetatable = getmetatable 25 | local getfenv = getfenv 26 | local debug, getsize, ioopen, corunning 27 | do 28 | local ok, dbg = pcall( require, "debug" ) 29 | if ok then debug = dbg end 30 | if not jit then 31 | local ok, getsz = pcall( require, "getsize" ) 32 | if ok then getsize = getsz end 33 | if not getsize and type( debug ) == "table" and 34 | type( debug.getsize ) == "function" then 35 | getsize = debug.getsize 36 | end 37 | end 38 | local ok, io = pcall( require, "io" ) 39 | if ok and type( io ) == "table" and 40 | type( io.open ) == "function" then 41 | ioopen = io.open 42 | end 43 | local ok, co = pcall( require, "coroutine" ) 44 | if ok and type( co ) == "table" and 45 | type( co.running ) == "function" then 46 | corunning = co.running 47 | end 48 | end 49 | 50 | 51 | local dottify 52 | local get_metatable, get_environment, get_registry, get_locals, upvalues 53 | 54 | 55 | -- select implementation of get_metatable depending on available API 56 | if type( debug ) == "table" and 57 | type( debug.getmetatable ) == "function" then 58 | 59 | local get_mt = debug.getmetatable 60 | function get_metatable( val, enabled ) 61 | if enabled then return get_mt( val ) end 62 | end 63 | 64 | elseif type( getmetatable ) == "function" then 65 | 66 | function get_metatable( val, enabled ) 67 | if enabled then return getmetatable( val ) end 68 | end 69 | 70 | else 71 | 72 | function get_metatable() end 73 | 74 | end 75 | 76 | 77 | -- select implementation of get_environment depending on available API 78 | if type( debug ) == "table" and 79 | type( debug.getfenv ) == "function" then 80 | 81 | local get_fe = debug.getfenv 82 | function get_environment( val, n, enabled ) 83 | if enabled and n == 1 then 84 | local uv = get_fe( val ) 85 | return uv, uv ~= nil 86 | end 87 | return nil, false 88 | end 89 | 90 | elseif type( debug ) == "table" and 91 | type( debug.getuservalue ) == "function" then 92 | 93 | local get_uv = debug.getuservalue 94 | if _VERSION < "Lua 5.4" then 95 | local get_uv_noindex = get_uv 96 | function get_uv( val, n ) 97 | if n == 1 then 98 | local uv = get_uv_noindex( val ) 99 | return uv, uv ~= nil 100 | end 101 | return nil, false 102 | end 103 | end 104 | function get_environment( val, n, enabled ) 105 | if enabled then 106 | -- getuservalue in Lua 5.2 throws on light userdata! 107 | local ok, res1, res2 = pcall( get_uv, val, n ) 108 | if ok then return res1, res2 end 109 | return nil, false 110 | end 111 | end 112 | 113 | elseif type( getfenv ) == "function" then 114 | 115 | function get_environment( val, n, enabled ) 116 | if enabled and n == 1 and type( val ) == "function" then 117 | local uv = getfenv( val ) 118 | return uv, uv ~= nil 119 | end 120 | return nil, false 121 | end 122 | 123 | else 124 | 125 | function get_environment() return nil, false end 126 | 127 | end 128 | 129 | 130 | -- select implementation of get_registry 131 | if type( debug ) == "table" and 132 | type( debug.getregistry ) == "function" then 133 | get_registry = debug.getregistry 134 | else 135 | function get_registry() end 136 | end 137 | 138 | 139 | -- select implementation of get_locals 140 | if type( debug ) == "table" and 141 | type( debug.getinfo ) == "function" and 142 | type( debug.getlocal ) == "function" then 143 | 144 | local getinfo, getlocal = debug.getinfo, debug.getlocal 145 | 146 | local function getinfo_nothread( _, func, what ) 147 | return getinfo( func, what ) 148 | end 149 | 150 | local function getlocal_nothread( _, level, loc ) 151 | return getlocal( level, loc ) 152 | end 153 | 154 | function get_locals( thread, enabled ) 155 | if enabled then 156 | local locs = {} 157 | local start = 1 158 | local gi, gl = getinfo, getlocal 159 | if not thread then 160 | gi, gl = getinfo_nothread, getlocal_nothread 161 | end 162 | local info, i = gi( thread, 0, "nf" ), 0 163 | while info do 164 | local t = { name = info.name, func = info.func } 165 | local j, n,v = 1, gl( thread, i, 1 ) 166 | while n ~= nil do 167 | t[ j ] = { n, v } 168 | j = j + 1 169 | n,v = gl( thread, i, j ) 170 | end 171 | i = i + 1 172 | locs[ i ] = t 173 | if info.func == dottify then start = i+1 end 174 | info = gi( thread, i, "nf" ) 175 | end 176 | return locs, start 177 | end 178 | end 179 | 180 | else 181 | 182 | function get_locals() end 183 | 184 | end 185 | 186 | 187 | -- select implementation of upvalues depending on available API 188 | local function dummy_iter() end 189 | if type( debug ) == "table" and 190 | type( debug.getupvalue ) == "function" then 191 | 192 | local get_up, uv_iter = debug.getupvalue 193 | if _VERSION == "Lua 5.1" then 194 | 195 | function uv_iter( state ) 196 | local name, uv = get_up( state.value, state.n ) 197 | state.n = state.n + 1 198 | return name, uv, nil 199 | end 200 | 201 | else -- Lua 5.2 (and later) mixes upvalues and environments 202 | 203 | local get_upid 204 | if type( debug.upvalueid ) == "function" then 205 | get_upid = debug.upvalueid 206 | end 207 | 208 | function uv_iter( state ) 209 | local name, uv = get_up( state.value, state.n ) 210 | state.n = state.n + 1 211 | if name == "_ENV" and not state.show_env then 212 | return uv_iter( state ) 213 | end 214 | local id = nil 215 | if get_upid ~= nil and name ~= nil then 216 | id = get_upid( state.value, state.n - 1 ) 217 | end 218 | return name, uv, id 219 | end 220 | end 221 | 222 | function upvalues( val, enabled, show_env ) 223 | if enabled then 224 | return uv_iter, { value = val, n = 1, show_env = show_env } 225 | else 226 | return dummy_iter 227 | end 228 | end 229 | 230 | else 231 | 232 | function upvalues() 233 | return dummy_iter 234 | end 235 | 236 | end 237 | 238 | 239 | local function ptostring( v ) 240 | local ok, res = pcall( tostring, v ) 241 | if ok then 242 | return res 243 | end 244 | local mt = get_metatable( v, true ) 245 | if type( mt ) == "table" then 246 | local tos = rawget( mt, "__tostring" ) 247 | rawset( mt, "__tostring", nil ) 248 | ok, res = pcall( tostring, v ) 249 | rawset( mt, "__tostring", tos ) 250 | if ok then 251 | return res 252 | end 253 | end 254 | return "" 255 | end 256 | 257 | 258 | 259 | -- scanning is done in breadth-first order using a linked list. the 260 | -- nodes are appended in ascending order of depth. there is also a 261 | -- lookup table by value (for reference types) or by upvalueid (for 262 | -- value type upvalues) to ensure a single node for a value 263 | local function new_db( proto ) 264 | proto = proto or {} 265 | proto.n_nodes = 0 266 | proto.list_begin = nil 267 | proto.list_end = nil 268 | proto.key2node = {} 269 | proto.max_depth = 0 270 | proto.prune = {} 271 | proto.edges = {} 272 | return proto 273 | end 274 | 275 | 276 | local function db_node( db, val, depth, key ) 277 | local node, t = nil, type( val ) 278 | if t ~= "number" and t ~= "boolean" and t ~= "nil" then 279 | key = val 280 | end 281 | if key ~= nil then 282 | node = db.key2node[ key ] 283 | end 284 | if not node and 285 | (db.max_depth < 1 or depth <= db.max_depth) and 286 | (key == nil or not db.prune[ key ]) then 287 | db.n_nodes = db.n_nodes + 1 288 | node = { 289 | id = db.n_nodes.."", 290 | value = val, 291 | depth = depth, 292 | shape = nil, label = nil, draw = nil, next = nil, 293 | } 294 | if key ~= nil then 295 | db.key2node[ key ] = node 296 | end 297 | if db.list_end ~= nil then 298 | db.list_end.next = node 299 | else 300 | db.list_begin = node 301 | end 302 | db.list_end = node 303 | end 304 | return node 305 | end 306 | 307 | 308 | local function define_edge( db, edge ) 309 | local es = db.edges 310 | es[ #es+1 ] = edge 311 | end 312 | 313 | 314 | -- generate dot code for references 315 | local function dottify_metatable_ref( src, port1, mt, port2, db ) 316 | define_edge( db, { 317 | A = src, A_port = port1, 318 | B = mt, B_port = port2, 319 | style = "dashed", 320 | dir = "both", 321 | arrowtail = "odiamond", 322 | label = "metatable", 323 | color = "blue" 324 | } ) 325 | src.draw, mt.draw = true, true 326 | end 327 | 328 | local function dottify_environment_ref( src, port1, env, port2, db ) 329 | define_edge( db, { 330 | A = src, A_port = port1, 331 | B = env, B_port = port2, 332 | style = "dotted", 333 | dir = "both", 334 | arrowtail = "dot", 335 | color = "red" 336 | } ) 337 | src.draw, env.draw = true, true 338 | end 339 | 340 | local function dottify_upvalue_ref( src, port1, upv, port2, db, name ) 341 | define_edge( db, { 342 | A = src, A_port = port1, 343 | B = upv, B_port = port2, 344 | style = "dashed", 345 | label = name, 346 | color = "green" 347 | } ) 348 | src.draw, upv.draw = true, true 349 | end 350 | 351 | local function dottify_ref( n1, port1, n2, port2, db ) 352 | define_edge( db, { 353 | A = n1, A_port = port1, 354 | B = n2, B_port = port2, 355 | style = "solid" 356 | } ) 357 | end 358 | 359 | local function dottify_stack_ref( th, port1, st, port2, db ) 360 | define_edge( db, { 361 | A = th, A_port = port1, 362 | B = st, B_port = port2, 363 | style = "solid", 364 | arrowhead = "none", 365 | weight = "2", 366 | color = "lightgrey", 367 | } ) 368 | th.draw = true 369 | end 370 | 371 | local function dottify_size_ref( n, port1, sn, port2, db ) 372 | define_edge( db, { 373 | A = n, A_port = port1, 374 | B = sn, B_port = port2, 375 | style = "dotted", 376 | label = "size", 377 | arrowhead = "none", 378 | color = "dimgrey", 379 | fontcolor = "dimgrey", 380 | fontsize = "10", 381 | } ) 382 | sn.draw = n.draw 383 | end 384 | 385 | 386 | local function abbrev( str ) 387 | if #str > max_label_length then 388 | str = ssub( str, 1, max_label_length-9 ).."..."..ssub( str, -6 ) 389 | end 390 | return str 391 | end 392 | 393 | 394 | -- escape and strings for graphviz labels 395 | local html_escapes = { 396 | [ "\r" ] = "\\r", 397 | [ "\n" ] = "\\n", 398 | [ "\t" ] = "\\t", 399 | [ "\f" ] = "\\f", 400 | [ "\v" ] = "\\v", 401 | [ "\\" ] = "\\\\", 402 | [ "'" ] = "\\'", 403 | [ "<" ] = "<", 404 | [ ">" ] = ">", 405 | [ "&" ] = "&", 406 | [ '"' ] = """, 407 | } 408 | local record_escapes = { 409 | [ "\r" ] = "\\\\r", 410 | [ "\n" ] = "\\\\n", 411 | [ "\t" ] = "\\\\t", 412 | [ "\f" ] = "\\\\f", 413 | [ "\v" ] = "\\\\v", 414 | [ "\\" ] = "\\\\\\\\", 415 | [ "'" ] = "\\\\'", 416 | [ "<" ] = "\\<", 417 | [ ">" ] = "\\>", 418 | [ '"' ] = '\\"', 419 | [ "{" ] = "\\{", 420 | [ "}" ] = "\\}", 421 | [ "|" ] = "\\|", 422 | } 423 | 424 | local function html_escaper( c ) 425 | return sformat( "\\%03d", sbyte( c ) ) 426 | end 427 | 428 | local function record_escaper( c ) 429 | return sformat( "\\\\%03d", sbyte( c ) ) 430 | end 431 | 432 | local function escape( str, use_html ) 433 | local esc 434 | if use_html then 435 | esc = html_escaper 436 | str = sgsub( str, "[\r\n\t\f\v\\'<>&\"]", html_escapes ) 437 | else 438 | esc = record_escaper 439 | str = sgsub( str, "[\r\n\t\f\v\\'<>\"{}|]", record_escapes ) 440 | end 441 | str = sgsub( str, "[^][%w !\"#$%%&'()*+,./:;<=>?@\\^_`{|}~-]", esc ) 442 | return str 443 | end 444 | 445 | 446 | local function quote( str ) 447 | return "'" .. str .. "'" 448 | end 449 | 450 | 451 | local function make_label_elem( tnode, v, db, subid, depth, alt ) 452 | local t = type( v ) 453 | if t == "number" or t == "boolean" then 454 | return escape( ptostring( v ), db.use_html ) 455 | elseif t == "string" then 456 | return quote( escape( abbrev( v ), db.use_html ) ) 457 | else -- userdata, function, thread, table 458 | local n = db_node( db, v, depth+1 ) 459 | if n then 460 | dottify_ref( tnode, subid, n, t == "table" and "0" or nil, db ) 461 | end 462 | alt = alt or ptostring( v ) 463 | return escape( abbrev( alt ), db.use_html ) 464 | end 465 | end 466 | 467 | 468 | local function make_html_table( db, node, val ) 469 | local depth = node.depth 470 | node.shape = "plaintext" 471 | node.is_html_label = true 472 | local header = escape( abbrev( ptostring( val ) ), true ) 473 | if getsize then 474 | header = header.." ["..getsize( val ).."]" 475 | end 476 | local label = [[ 477 | 479 | ]] 480 | local handled = {} 481 | -- first the array part 482 | local n, v = 1, rawget( val, 1 ) 483 | while v ~= nil do 484 | local el_label = make_label_elem( node, v, db, n.."", depth ) 485 | label = label .. [[ 486 | 488 | ]] 489 | handled[ n ] = true 490 | n = n + 1 491 | v = rawget( val, n ) 492 | end 493 | -- and then the hash part 494 | for k,v in next, val do 495 | node.draw = true 496 | if not handled[ k ] then -- skip array part elements 497 | local k_label = make_label_elem( node, k, db, "k"..n, depth ) 498 | local v_label = make_label_elem( node, v, db, "v"..n, depth ) 499 | label = label .. [[ 500 | 503 | ]] 504 | n = n + 1 505 | end 506 | end 507 | node.label = label .. [[
]] .. header .. [[ 478 |
]] .. el_label .. [[ 487 |
]] .. k_label .. [[ 501 | ]] .. v_label .. [[ 502 |
]] 508 | end 509 | 510 | 511 | local function make_record_table( db, node, val ) 512 | local depth = node.depth 513 | node.shape = "record" 514 | local label = "{ <0> " .. escape( abbrev( ptostring( val ) ), false ) 515 | if getsize then 516 | label = label.." ["..getsize( val ).."]" 517 | end 518 | local handled = {} 519 | -- first the array part 520 | local n,v = 1, rawget( val, 1 ) 521 | while v ~= nil do 522 | local el_label = make_label_elem( node, v, db, n.."", depth ) 523 | label = label .. " | <" .. n .. "> " .. el_label 524 | handled[ n ] = true 525 | n = n + 1 526 | v = rawget( val, n ) 527 | end 528 | -- and then the hash part 529 | local keys, values = {}, {} 530 | for k,v in next, val do 531 | node.draw = true 532 | if not handled[ k ] then -- skip array part elements 533 | local k_label = make_label_elem( node, k, db, "k"..n, depth ) 534 | local v_label = make_label_elem( node, v, db, "v"..n, depth ) 535 | keys[ #keys+1 ] = " " .. k_label 536 | values[ #values+1 ] = " " .. v_label 537 | n = n + 1 538 | end 539 | end 540 | if next( keys ) ~= nil then 541 | label = label .. " | { { " .. tconcat( keys, " | " ) .. 542 | " } | { " .. tconcat( values, " | " ) .. " } }" 543 | end 544 | node.label = label .. " }" 545 | end 546 | 547 | 548 | local function make_html_stack( db, node ) 549 | local frames, start = get_locals( node.thread, db.show_locals ) 550 | if frames then 551 | local depth = node.depth 552 | local n = 0 553 | node.shape = "plaintext" 554 | node.is_html_label = true 555 | local label = [[ 556 | 557 | ]] 558 | for i = start, #frames do 559 | local frame = frames[ i ] 560 | local name, func = frame.name, frame.func 561 | if name == '' and i == #frames then name = "[coroutine init]" end 562 | label = label .. ' \n' 566 | n = n + 1 567 | for i = #frame, 1, -1 do 568 | label = label .. ' \n' 574 | n = n + 1 575 | node.draw = true 576 | end 577 | end 578 | node.label = label .. [[
' .. 564 | make_label_elem( node, func, db, n..":e", depth, name ) .. 565 | '
' .. 569 | escape( i.."", true ) .. '' .. 570 | escape( abbrev( frame[ i ][ 1 ] ), true ) .. 571 | '' .. 572 | make_label_elem( node, frame[ i ][ 2 ], db, n, depth ) .. 573 | '
]] 579 | end 580 | end 581 | 582 | 583 | local function make_record_stack( db, node ) 584 | local frames, start = get_locals( node.thread, db.show_locals ) 585 | if frames then 586 | local depth = node.depth 587 | local n = 0 588 | node.shape = "Mrecord" 589 | node.color = "lightgrey" 590 | local label = "{" 591 | for i = start, #frames do 592 | local frame = frames[ i ] 593 | local name, func = frame.name, frame.func 594 | if name == '' and i == #frames then name = "[coroutine init]" end 595 | if n > 0 then label = label .. " |" end 596 | label = label .. " <" .. n .. "> " .. 597 | make_label_elem( node, func, db, n..":e", depth, name ) 598 | n = n + 1 599 | local nums, keys, values = {}, {}, {} 600 | for i = #frame, 1, -1 do 601 | nums[ #nums+1 ] = escape( i.."", false ) 602 | keys[ #keys+1 ] = escape( abbrev( frame[ i ][ 1 ] ), false ) 603 | values[ #values+1 ] = "<" .. n .. "> " .. 604 | make_label_elem( node, frame[ i ][ 2 ], db, n, depth ) 605 | n = n + 1 606 | node.draw = true 607 | end 608 | if next( nums ) ~= nil then 609 | label = label .. " | { { " .. tconcat( nums, " | " ) .. 610 | " } | { " .. tconcat( keys, " | " ) .. " } | { " .. 611 | tconcat( values, " | " ) .. " } }" 612 | end 613 | end 614 | node.label = label .. " }" 615 | end 616 | end 617 | 618 | 619 | local function handle_metatable( db, node, val ) 620 | local mt = get_metatable( val, db.show_metatables ) 621 | if mt ~= nil then 622 | local mt_node = db_node( db, mt, node.depth+1 ) 623 | if mt_node then 624 | local r = type( mt ) == "table" and "0" or nil 625 | dottify_metatable_ref( node, nil, mt_node, r, db ) 626 | end 627 | end 628 | end 629 | 630 | local function handle_environment( db, node, val ) 631 | local n = 0 632 | repeat 633 | n = n + 1 634 | local env, has_env = get_environment( val, n, db.show_environments ) 635 | if has_env and env ~= nil then 636 | local env_node = db_node( db, env, node.depth+1 ) 637 | if env_node then 638 | local r = type( env ) == "table" and "0" or nil 639 | dottify_environment_ref( node, nil, env_node, r, db ) 640 | end 641 | end 642 | until not has_env 643 | end 644 | 645 | local function handle_upvalues( db, node, val ) 646 | for na,uv,id in upvalues( val, db.show_upvalues, db.show_environments ) do 647 | local uv_node = db_node( db, uv, node.depth+1, id ) 648 | if uv_node then 649 | local r = type( uv ) == "table" and "0" or nil 650 | dottify_upvalue_ref( node, nil, uv_node, r, db, na ) 651 | end 652 | end 653 | end 654 | 655 | local function handle_stack( db, node, val ) 656 | if db.show_locals then 657 | local id = db[ val ] or {} 658 | local st = db_node( db, id, node.depth ) 659 | st.cb = "stack" 660 | st.thread = val 661 | dottify_stack_ref( node, nil, st, "0", db ) 662 | end 663 | end 664 | 665 | local function handle_registry( db ) 666 | if db.show_registry then 667 | local reg = get_registry() 668 | if type( reg ) == "table" then 669 | local re = db_node( db, reg, 1 ) 670 | re.draw = true 671 | end 672 | end 673 | end 674 | 675 | local function handle_main_stack( db ) 676 | if db.show_locals then 677 | local id = {} 678 | local st = db_node( db, id, 1 ) 679 | if corunning then 680 | local th = corunning() 681 | if th then 682 | db[ th ] = id 683 | end 684 | end 685 | st.cb = "stack" 686 | end 687 | end 688 | 689 | local function handle_size( db, node, val ) 690 | if db.show_sizes and getsize then 691 | local sn = db_node( db, getsize( val ), node.depth, {} ) 692 | sn.cb = "size" 693 | dottify_size_ref( node, nil, sn, nil, db ) 694 | end 695 | end 696 | 697 | 698 | local function dottify_table( db, node, val ) 699 | if db.use_html then 700 | make_html_table( db, node, val ) 701 | else 702 | make_record_table( db, node, val ) 703 | end 704 | handle_metatable( db, node, val ) 705 | handle_size( db, node, val ) 706 | end 707 | 708 | 709 | local function dottify_userdata( db, node, val ) 710 | local label = escape( abbrev( ptostring( val ) ), false ) 711 | if getsize then 712 | label = label.." ["..getsize( val ).."]" 713 | end 714 | node.label = label 715 | node.shape = "box" 716 | node.height = "0.3" 717 | handle_metatable( db, node, val ) 718 | handle_environment( db, node, val ) 719 | handle_size( db, node, val ) 720 | end 721 | 722 | 723 | local function dottify_cdata( db, node, val ) 724 | node.label = escape( abbrev( ptostring( val ) ), false ) 725 | node.shape = "parallelogram" 726 | node.margin = "0.01" 727 | node.width = "0.3" 728 | node.height = "0.3" 729 | -- cdata objects *do* have a metatable but it's always 730 | -- the same, so it's not really interesting ... 731 | -- handle_metatable( db, node, val ) 732 | end 733 | 734 | 735 | local function dottify_thread( db, node, val ) 736 | local label = escape( abbrev( ptostring( val ) ), false ) 737 | node.group = label 738 | if getsize then 739 | label = label.." ["..getsize( val ).."]" 740 | end 741 | node.label = label 742 | node.shape = "octagon" 743 | node.margin = "0.01" 744 | node.width = "0.3" 745 | node.height = "0.3" 746 | handle_environment( db, node, val ) 747 | handle_stack( db, node, val ) 748 | handle_size( db, node, val ) 749 | end 750 | 751 | 752 | local function dottify_function( db, node, val ) 753 | local label = escape( abbrev( ptostring( val ) ), false ) 754 | if getsize then 755 | label = label.." ["..getsize( val ).."]" 756 | end 757 | node.label = label 758 | node.shape = "ellipse" 759 | node.margin = "0.01" 760 | node.height = "0.3" 761 | handle_environment( db, node, val ) 762 | handle_upvalues( db, node, val ) 763 | handle_size( db, node, val ) 764 | end 765 | 766 | 767 | local function dottify_string( db, node, val ) 768 | node.label = quote( escape( abbrev( val ), false ) ) 769 | node.shape = "plaintext" 770 | end 771 | 772 | 773 | local function dottify_other( db, node, val ) 774 | node.label = escape( abbrev( ptostring( val ) ), false ) 775 | node.shape = "plaintext" 776 | end 777 | 778 | 779 | local function dottify_stack( db, node ) 780 | if node.thread then 781 | node.group = escape( abbrev( ptostring( node.thread ) ), false ) 782 | end 783 | if db.use_html then 784 | make_html_stack( db, node ) 785 | else 786 | make_record_stack( db, node ) 787 | end 788 | end 789 | 790 | 791 | local function dottify_size( db, node, val ) 792 | node.label = escape( abbrev( val.."" ), false ) 793 | node.shape = "circle" 794 | node.width = "0.3" 795 | node.margin = "0.01" 796 | node.color = "lightgrey" 797 | node.fontcolor = "dimgrey" 798 | node.fontsize = "10" 799 | end 800 | 801 | 802 | local callbacks = { 803 | table = dottify_table, 804 | [ "function" ] = dottify_function, 805 | thread = dottify_thread, 806 | userdata = dottify_userdata, 807 | string = dottify_string, 808 | number = dottify_other, 809 | boolean = dottify_other, 810 | [ "nil" ] = dottify_other, 811 | stack = dottify_stack, 812 | cdata = dottify_cdata, 813 | size = dottify_size, 814 | } 815 | 816 | local function dottify_go( db, val ) 817 | handle_registry( db ) 818 | handle_main_stack( db ) 819 | db_node( db, val, 1 ).draw = true 820 | local node = db.list_begin 821 | while node do 822 | callbacks[ node.cb or type( node.value ) ]( db, node, node.value ) 823 | node = node.next 824 | end 825 | end 826 | 827 | 828 | local function write_nodes( db, out_f ) 829 | local node = db.list_begin 830 | while node do 831 | if db.show_leaves or node.draw then 832 | out_f( db, node ) 833 | end 834 | node = node.next 835 | end 836 | end 837 | 838 | 839 | local function write_edges( db, out_f ) 840 | for i = 1, #db.edges do 841 | local e = db.edges[ i ] 842 | local n1, n2 = e.A, e.B 843 | if db.show_leaves or (n1.draw and n2.draw) then 844 | out_f( db, e, n1, n2 ) 845 | end 846 | end 847 | end 848 | 849 | 850 | local option_names = { 851 | "label", "shape", "style", "dir", "arrowhead", "arrowtail", "color", 852 | "margin", "group", "weight", "fontcolor", "fontsize", "width", 853 | "height", 854 | } 855 | 856 | local function process_options_as_text( obj ) 857 | local options = {} 858 | for i = 1, #option_names do 859 | local opt = option_names[ i ] 860 | if obj[ opt ] then 861 | local quote_on = "\"" 862 | local quote_off = "\"" 863 | if opt == "label" and obj.is_html_label then 864 | quote_on, quote_off = "<", ">" 865 | end 866 | options[ #options+1 ] = opt .. "=" .. quote_on .. 867 | obj[ opt ] .. quote_off 868 | end 869 | end 870 | return options 871 | end 872 | 873 | 874 | local function write_graph_as_text( db, out ) 875 | out( "digraph G {" ) 876 | if db.label then 877 | out( " label=\"" .. escape( db.label, false ) .. "\";" ) 878 | end 879 | write_nodes( db, function( db, n ) 880 | local options = process_options_as_text( n ) 881 | out( " " .. n.id .. " [" .. tconcat( options, "," ) .. "];" ) 882 | end ) 883 | write_edges( db, function( db, e, n1, n2 ) 884 | local id1 = n1.id 885 | if e.A_port then id1 = id1 .. ":" .. e.A_port end 886 | local id2 = n2.id 887 | if e.B_port then id2 = id2 .. ":" .. e.B_port end 888 | local options = process_options_as_text( e ) 889 | out( " " .. id1 .. " -> " .. id2 .. " [" .. 890 | tconcat( options, "," ) .. "];" ) 891 | end ) 892 | out( "}" ) 893 | end 894 | 895 | 896 | local gvd_options = { 897 | metatables = function( db ) db.show_metatables = true end, 898 | nometatables = function( db ) db.show_metatables = nil end, 899 | upvalues = function( db ) db.show_upvalues = true end, 900 | noupvalues = function( db ) db.show_upvalues = nil end, 901 | environments = function( db ) db.show_environments = true end, 902 | noenvironments = function( db ) db.show_environments = nil end, 903 | html = function( db ) db.use_html = true end, 904 | nohtml = function( db ) db.use_html = nil end, 905 | leaves = function( db ) db.show_leaves = true end, 906 | noleaves = function( db ) db.show_leaves = nil end, 907 | registry = function( db ) db.show_registry = true end, 908 | noregistry = function( db ) db.show_registry = nil end, 909 | locals = function( db ) db.show_locals = true end, 910 | nolocals = function( db ) db.show_locals = nil end, 911 | sizes = function( db ) db.show_sizes = true end, 912 | nosizes = function( db ) db.show_sizes = nil end, 913 | } 914 | 915 | local function default_option( db, opt ) 916 | local t = type( opt ) 917 | if t == "number" then 918 | db.max_depth = opt 919 | elseif t == "table" or t == "userdata" or 920 | t == "function" or t == "thread" then 921 | db.prune[ opt ] = true 922 | elseif t == "string" then 923 | db.label = opt 924 | end 925 | end 926 | 927 | 928 | -- main function (predeclared above) 929 | function dottify( output, val, ... ) 930 | local db = new_db{ 931 | show_metatables = true, 932 | show_upvalues = true, 933 | use_html = true, 934 | } 935 | for i = 1, select( '#', ... ) do 936 | local opt = select( i, ... ); 937 | (gvd_options[ opt ] or default_option)( db, opt ) 938 | end 939 | dottify_go( db, val ) 940 | if type( output ) == "string" then 941 | assert( ioopen, "io.open needs to be defined for file output" ) 942 | local file = assert( ioopen( output, "w" ) ) 943 | write_graph_as_text( db, function( s ) 944 | file:write( s, "\n" ) 945 | end ) 946 | file:close() 947 | else 948 | write_graph_as_text( db, output ) 949 | end 950 | end 951 | 952 | return dottify 953 | 954 | --------------------------------------------------------------------------------