Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 131 additions & 47 deletions builtin/common/misc_helpers.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,15 @@ local math = math
local function basic_dump(o)
local tp = type(o)
if tp == "number" then
return tostring(o)
local s = tostring(o)
if tonumber(s) == o then
return s
end
-- Prefer an exact representation over a compact representation.
-- e.g. basic_dump(0.3) == "0.3",
-- but basic_dump(0.1 + 0.2) == "0.30000000000000004"
-- so the user can see that 0.1 + 0.2 ~= 0.3
return string.format("%.17g", o)
elseif tp == "string" then
return string.format("%q", o)
elseif tp == "boolean" then
Expand Down Expand Up @@ -100,65 +108,141 @@ function dump2(o, name, dumped)
return string.format("%s = {}\n%s", name, table.concat(t))
end

--------------------------------------------------------------------------------
-- This dumps values in a one-statement format.

-- This dumps values in a human-readable expression format.
-- If possible, the resulting string should evaluate to an equivalent value if loaded and executed.
-- For example, {test = {"Testing..."}} becomes:
-- [[{
-- test = {
-- "Testing..."
-- }
-- }]]
-- This supports tables as keys, but not circular references.
-- It performs poorly with multiple references as it writes out the full
-- table each time.
-- The indent field specifies a indentation string, it defaults to a tab.
-- Use the empty string to disable indentation.
-- The dumped and level arguments are internal-only.

function dump(o, indent, nested, level)
local t = type(o)
if not level and t == "userdata" then
-- when userdata (e.g. player) is passed directly, print its metatable:
return "userdata metatable: " .. dump(getmetatable(o))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed this undocumented "feature". I think if modders want this, they should do dump(getmetatable(value)) explicitly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was an explicitly requested feature: #6842

re "undocumented": documenting the precise behavior of a pure debugging helper function isn't needed and can only cause us problems in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was an explicitly requested feature: #6842

i wonder whether @HybridDog would still consider this feature a good idea today?


documenting the precise behavior of a pure debugging helper function isn't needed and can only cause us problems in the future.

sure. i'm just trying to argue that i should be somewhat free to remove things like this if i can make a half-decent case that they should be removed.

ultimately i don't mind putting it back in terribly much but i still think it shouldn't work like that. dump({player}, "") gives you {<userdata>,} but dump(player, "") dumps you the (usually annoyingly big) metatable? that's just confusing if you ask me, it breaks the natural recursive nature of dump and adds a weird special case.


we should probably give our userdata proper __tostring implementations which identify it. e.g. PlayerRef: singleplayer or something.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm yeah if it doesn't work recursively I agree it's not ideal.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if a modder uses dump() on a variable to find out what it is, an output like userdata: 0x7acd0162a3e0 is not very helpful to him/her and showing the metatable is also not ideal. Removing the feature to print the metatable is fine for me.
Adding __tostring implementations for userdata sounds like a better solution if this is possible.

end
if t ~= "table" then
return basic_dump(o)
function dump(value, indent)
indent = indent or "\t"
local newline = indent == "" and "" or "\n"

local rope = {}
local write
do
-- Keeping the length of the table as a local variable is *much*
-- faster than invoking the length operator.
-- See https://gitspartv.github.io/LuaJIT-Benchmarks/#test12.
local i = 0
function write(str)
i = i + 1
rope[i] = str
end
end

-- Contains table -> true/nil of currently nested tables
nested = nested or {}
if nested[o] then
return "<circular reference>"
local n_refs = {}
local function count_refs(val)
if type(val) ~= "table" then
return
end
local tbl = val
if n_refs[tbl] then
n_refs[tbl] = n_refs[tbl] + 1
return
end
n_refs[tbl] = 1
for k, v in pairs(tbl) do
count_refs(k)
count_refs(v)
end
end
nested[o] = true
indent = indent or "\t"
level = level or 1
count_refs(value)

local ret = {}
local dumped_indexes = {}
for i, v in ipairs(o) do
ret[#ret + 1] = dump(v, indent, nested, level + 1)
dumped_indexes[i] = true
end
for k, v in pairs(o) do
if not dumped_indexes[k] then
if type(k) ~= "string" or not is_valid_identifier(k) then
k = "["..dump(k, indent, nested, level + 1).."]"
local refs = {}
local cur_ref = 1
local function write_value(val, level)
if type(val) ~= "table" then
write(basic_dump(val))
return
end

local tbl = val
if refs[tbl] then
write(refs[tbl])
return
end

if n_refs[val] > 1 then
refs[val] = ("getref(%d)"):format(cur_ref)
write(("setref(%d)"):format(cur_ref))
cur_ref = cur_ref + 1
end
write("{")
if next(tbl) == nil then
write("}")
return
end
write(newline)

local function write_entry(k, v)
write(indent:rep(level))
write("[")
write_value(k, level + 1)
write("] = ")
write_value(v, level + 1)
write(",")
write(newline)
end

local keys = {string = {}, number = {}}
for k in pairs(tbl) do
local t = type(k)
if keys[t] then
table.insert(keys[t], k)
end
v = dump(v, indent, nested, level + 1)
ret[#ret + 1] = k.." = "..v
end

-- Write string-keyed entries
table.sort(keys.string)
for _, k in ipairs(keys.string) do
local v = val[k]
if is_valid_identifier(k) then
write(indent:rep(level))
write(k)
write(" = ")
write_value(v, level + 1)
write(",")
write(newline)
else
write_entry(k, v)
end
end

-- Write number-keyed entries
local len = 0
for i in ipairs(tbl) do
len = i
end
if #keys.number == len then -- table is a list
for _, v in ipairs(tbl) do
write(indent:rep(level))
write_value(v, level + 1)
write(",")
write(newline)
end
else -- table harbors arbitrary number keys
table.sort(keys.number)
for _, k in ipairs(keys.number) do
write_entry(k, tbl[k])
end
end

-- Write all remaining entries
for k, v in pairs(val) do
if not keys[type(k)] then
write_entry(k, v)
end
end

write(indent:rep(level - 1))
write("}")
end
nested[o] = nil
if indent ~= "" then
local indent_str = "\n"..string.rep(indent, level)
local end_indent_str = "\n"..string.rep(indent, level - 1)
return string.format("{%s%s%s}",
indent_str,
table.concat(ret, ","..indent_str),
end_indent_str)
end
return "{"..table.concat(ret, ", ").."}"
write_value(value, 1)
return table.concat(rope)
end

--------------------------------------------------------------------------------
Expand Down
8 changes: 6 additions & 2 deletions builtin/common/serialize.lua
Original file line number Diff line number Diff line change
Expand Up @@ -218,9 +218,13 @@ function core.serialize(value)
core.log("deprecated", "Support for dumping functions in `core.serialize` is deprecated.")
end
local rope = {}
-- Keeping the length of the table as a local variable is *much*
-- faster than invoking the length operator.
-- See https://gitspartv.github.io/LuaJIT-Benchmarks/#test12.
local i = 0
serialize(value, function(text)
-- Faster than table.insert(rope, text) on PUC Lua 5.1
rope[#rope + 1] = text
i = i + 1
rope[i] = text
end)
return table_concat(rope)
end
Expand Down
121 changes: 121 additions & 0 deletions builtin/common/tests/misc_helpers_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -230,3 +230,124 @@ describe("math", function()
assert.equal(0, math.round(-0.49999999999999994))
end)
end)

describe("dump", function()
local function test_expression(expr)
local chunk = assert(loadstring("return " .. expr))
local refs = {}
setfenv(chunk, {
setref = function(id)
refs[id] = {}
return function(fields)
for k, v in pairs(fields) do
refs[id][k] = v
end
return refs[id]
end
end,
getref = function(id)
return assert(refs[id])
end,
})
assert.equal(expr, dump(chunk()))
end

it("nil", function()
test_expression("nil")
end)

it("booleans", function()
test_expression("false")
test_expression("true")
end)

describe("numbers", function()
it("formats integers nicely", function()
test_expression("42")
end)
it("avoids misleading rounding", function()
test_expression("0.3")
assert.equal("0.30000000000000004", dump(0.1 + 0.2))
end)
end)

it("strings", function()
test_expression('"hello world"')
test_expression([["hello \"world\""]])
end)

describe("tables", function()
it("empty", function()
test_expression("{}")
end)

it("lists", function()
test_expression([[
{
false,
true,
"foo",
1,
2,
}]])
end)

it("number keys", function()
test_expression([[
{
[0.5] = false,
[1.5] = true,
[2.5] = "foo",
}]])
end)

it("dicts", function()
test_expression([[{
a = 1,
b = 2,
c = 3,
}]])
end)

it("mixed", function()
test_expression([[{
a = 1,
b = 2,
c = 3,
["d e"] = true,
"foo",
"bar",
}]])
end)

it("nested", function()
test_expression([[{
a = {
1,
{},
},
b = "foo",
c = {
[0.5] = 0.1,
[1.5] = 0.2,
},
}]])
end)

it("circular references", function()
test_expression([[setref(1){
child = {
parent = getref(1),
},
other_child = {
parent = getref(1),
},
}]])
end)

it("supports variable indent", function()
assert.equal('{1,2,3,{foo = "bar",},}', dump({1, 2, 3, {foo = "bar"}}, ""))
assert.equal('{\n "x",\n "y",\n}', dump({"x", "y"}, " "))
end)
end)
end)
13 changes: 8 additions & 5 deletions doc/lua_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4162,9 +4162,11 @@ Helper functions
* `obj`: arbitrary variable
* `name`: string, default: `"_"`
* `dumped`: table, default: `{}`
* `dump(obj, dumped)`: returns a string which makes `obj` human-readable
* `obj`: arbitrary variable
* `dumped`: table, default: `{}`
* `dump(value, indent)`: returns a string which makes `value` human-readable
* `value`: arbitrary value
* Circular references are supported. Every table is dumped only once.
* `indent`: string to use for indentation, default: `"\t"`
* `""` disables indentation & line breaks (compact output)
* `math.hypot(x, y)`
* Get the hypotenuse of a triangle with legs x and y.
Useful for distance calculation.
Expand Down Expand Up @@ -7609,9 +7611,10 @@ Misc.
* Example: `write_json({10, {a = false}})`,
returns `'[10, {"a": false}]'`
* `core.serialize(table)`: returns a string
* Convert a table containing tables, strings, numbers, booleans and `nil`s
into string form readable by `core.deserialize`
* Convert a value into string form readable by `core.deserialize`.
* Supports tables, strings, numbers, booleans and `nil`.
* Support for dumping function bytecode is **deprecated**.
* Note: To obtain a human-readable representation of a value, use `dump` instead.
* Example: `serialize({foo="bar"})`, returns `'return { ["foo"] = "bar" }'`
* `core.deserialize(string[, safe])`: returns a table
* Convert a string returned by `core.serialize` into a table
Expand Down
2 changes: 1 addition & 1 deletion src/script/lua_api/l_areastore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ void LuaAreaStore::Register(lua_State *L)
{"__gc", gc_object},
{0, 0}
};
registerClass(L, className, methods, metamethods);
registerClass<LuaAreaStore>(L, methods, metamethods);

// Can be created from Lua (AreaStore())
lua_register(L, className, create_object);
Expand Down
Loading
Loading