Function to dump tables to debug output

It would be cool for debugging if print() would dump the contents of a table similar to print_r in PHP (PHP: print_r - Manual).

Right now if I have a Lua object like this:

local myObj = { Health = 100, Inventory = {Bullets = 23, Dynamite = 50} }

In the output console if I print(myObj) I will get something like

table: 255A62F8

It would be much more useful if I got:

myObj (table: 255A62F8) => { Health = 100, Inventory = (table: 255A62FF) => { Bullets = 23, Dynamite = 50 } }

This output is kind of similar to HttpService:JSONEncode except that it would handle mixed tables.

EDIT: Discourse messed up my indenting so imagine if this output was nicely indented.

17 Likes

I think @EchoReaper has a function that will print the content of a table like that. Sounds like a great thing to upload as a Model so it’s always a quick require away

It would be much better as a built in function that you could call from the output console

1 Like

I do have a pretty hastily developed function to print tables url=http://pastebin.com/X6y9cdzr[/url] that I ripped out of a plugin meant to read and generate tables from and to strings for the purpose of saving settings, but it’d be super useful if table contents were printed out by default.

3 Likes

How to handle cyclic tables though?
I once wrote something similar, that would change this:

local b = {'hi'}
b.loop = b
local a = {c={b=b},d={b=b}}

into:

{
	c = {
		b = {
			'hi',
			loop = c.b
		}
	},
	d = {
		b = c.b
	}
}

(maybe “a.c.b” instead of “c.b”, I forgot)
(It just prints the first reference when a table is seen multiple times)
(well I would probably have “TABLE c.b” so I know it’s a table etc)

(it’s tricky)

@Shedletsky: What about making this function a member of table, e.g. table.dump or table.debugString or table.dumpString? (that returns a string and/or prints it) I don’t think print by itself should provide this functionality, because everyone expects it to print one line containing just the memory address of the table. Then again, if we do that it’s just as convenient as using HttpService’s JSON encode function.

@einsteink: The function can keep track of the tables it has already printed, and if it encounters a table it has already printed it can simply just print the memory address and then be done with it. e.g.:

someTable (table: 1234ABCD) => {
   NonCyclicReference = (table: 5678ABCD) => {
      (...)
   }
   CyclicReference = (table: 1234ABCD),
   (...)
}

Where 1234ABCD is the memory address of the cyclic table

1 Like

A bit like my example, also explained in a way it’s difficult to understand.
(although your example code makes a bit more sense)

For my use case, which is debugging simple objects it would be sufficient to print out only the first 100-200 lines of the contents of the table, so in a cyclical table it would be ok to barf the whole thing out.

As you point out, though, it seems possible to detect cyclical tables and trim the display of those, using some graph traversal algo.

Even cooler would be a collapsible representation for nested tables similar to what the Visual Studio debugger provides. Then you just ignore this problem.

I’m just gonna say “yes, exactly” although I have no idea what that algorythm is.
It’s basicly “did I print this table already? k, only print reference or something”

Breadth-first search with a taboo list. The taboo list is all of the table references you have seen before. When you encounter a taboo node in the search, you ignore it or mark it as a cycle.

Any memory managed language with a garbage collector (Java, C#, Lua) solves this problem, so any of those algos would work.

A bit like this?

local function tableToString(tab,a,b)
	a,b = a or 0, b or {[tab]="ROOT"}
	local name = b[tab]
	local white = ("\t"):rep(a+1)
	local res = {"{"}
	for k,v in pairs(tab) do
		local value
		if b[v] then
			value = b[v]
		elseif type(v) == "table" then
			b[v] = name.."."..tostring(k)
			value = tableToString(v,a+1,b)
		elseif type(v) == "string" then
			value = '"'..v:gsub("\n","\\n"):gsub("\t","\\t")..'"'
		else
			value = tostring(v)
		end
		table.insert(res,white..tostring(k).." = "..value)
	end white = white:sub(2)
	table.insert(res,white.."}")
	return table.concat(res,"\n")
end

local test = {
	str = "Result:\n\t- Unknown";
	number = 12345;
	child = {a="b"};
	cyclic = {};
}
test.child.cyclic = test.cyclic
test.another = test.child

print(tableToString(test))

Gives this:

{
	number = 12345
	another = {
		a = "b"
		cyclic = {
		}
	}
	child = ROOT.another
	cyclic = ROOT.another.cyclic
	str = "Result:\n\t- Unknown"
}

Of course, naming the tables might not go… that well…
Now I understand what you mean:
First get all names of all the tables using breadth-first-magic.
I might write that in a minute.

1 Like

Not as easy as I expected:


local function getNames(tab,name,res,lev)
	res = res or {[tab]="ROOT"}
	local pls = {} lev = lev or 0
	for k,v in pairs(tab) do
		if type(v) == "table" and not res[v] then
			local n = name.."."..tostring(k)
			res[v] = n pls[v] = n
		end
	end
	for k,v in pairs(pls) do
		getNames(k,v,res)
		pls[k] = lev
	end return res,pls
end

local function tableToString(tab,a,b,c,d)
	a,b = a or 0, b or {[tab]=true}
	local name = b[tab]
	local white = ("\t"):rep(a+1)
	if not c then
		c,d = getNames(tab,"ROOT")
	end local res = {"{"}
	for k,v in pairs(tab) do
		local value
		if type(v) == "table" then
			if d[v] == a and not b[v] then
				b[v] = true
				value = tableToString(v,a+1,b,c,d)
			else
				value = c[v]
			end
		elseif type(v) == "string" then
			value = '"'..v:gsub("\n","\\n"):gsub("\t","\\t")..'"'
		else
			value = tostring(v)
		end
		table.insert(res,white..tostring(k).." = "..value)
	end white = white:sub(2)
	table.insert(res,white.."}")
	return table.concat(res,"\n")
end

local test = {
	str = "Result:\n\t- Unknown";
	number = 12345;
	child = {a="b"};
	cyclic = {};
}
test.child.cyclic = test.cyclic
test.another = test.child

print(tableToString(test))

Result:

{
	number = 12345
	another = {
		a = "b"
		cyclic = ROOT.cyclic
	}
	child = ROOT.another
	cyclic = {
	}
	str = "Result:\n\t- Unknown"
}
1 Like

I have thought about it a bit more and I believe that a depth first search would probably give more readable output most of the time.

I’m thinking about the case where your table has a lot of cycles. You’d want the “also this cycle” tags to all be at the bottom of your output instead of strewn throughout.

For anyone that cares, gmods ToString function ( and other table utilities ):

1 Like

Gives me this output for my test table:

ROOT	=	{
		number	=	12345,
		another	=	{
			a	=	"b",
			another	=	{
					},
			cyclic	=	{
					},
				},
		child	=	table: 1EAE5130,
		cyclic	=	table: 1EAE5250,
		inceptionish	=	table: 1EAE50D0,
		str	=	"Result:
	- Unknown",
}

(after converting to real Lua… why does GMod call the not-Lua Lua?)

Very similar to mine actually, but without the whole “print reference of table in a way we understand”

The name is confusing. Is it entailing a separate function to print out tables, or for print to be able to print out tables?

Well there is already HttpService:JSONEncode, so doing any more work here only makes sense if you are willing to change the default behavior of print I think.

Might as well throw my implementation into the mix.

It detects arrays, keys that are variables, and it sorts by value type. I think it also prints out valid Lua syntax.

1 Like

The original request was for it to work with print directly, so that’s what the card is for. I’ll update the card to specify.

2 Likes