diff --git a/CHANGELOG.md b/CHANGELOG.md index c87fd19..94a7cca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## v3.1.3 + +This is a maintenance release that should be completely backwards-compatible and introduces no new +features. + +* inspect.lua was rewritten using Teal (#52) +* A minimal performance test was introduced. Then several were made to the code, which seems to be faster now. + ## v3.1.2 * DEL character is properly escaped in strings (#49, thanks @4mig4 and @LoganDark for the bug report) diff --git a/Makefile b/Makefile index 7e792c9..1fc1846 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,7 @@ +.PHONY: all dev gen check test perf + +LUA := $(shell luarocks config lua_interpreter) + all: gen check test dev: @@ -14,5 +18,8 @@ check: test: busted +perf: + $(shell luarocks config lua_interpreter) perf.lua + diff --git a/inspect.lua b/inspect.lua index dea6d6f..f8d69dc 100644 --- a/inspect.lua +++ b/inspect.lua @@ -48,6 +48,11 @@ inspect.KEY = setmetatable({}, { __tostring = function() return 'inspect.KEY' en inspect.METATABLE = setmetatable({}, { __tostring = function() return 'inspect.METATABLE' end }) local tostring = tostring +local rep = string.rep +local match = string.match +local char = string.char +local gsub = string.gsub +local fmt = string.format local function rawpairs(t) return next, t, nil @@ -56,10 +61,10 @@ end local function smartQuote(str) - if str:match('"') and not str:match("'") then + if match(str, '"') and not match(str, "'") then return "'" .. str .. "'" end - return '"' .. str:gsub('"', '\\"') .. '"' + return '"' .. gsub(str, '"', '\\"') .. '"' end @@ -69,17 +74,17 @@ local shortControlCharEscapes = { } local longControlCharEscapes = { ["\127"] = "\127" } for i = 0, 31 do - local ch = string.char(i) + local ch = char(i) if not shortControlCharEscapes[ch] then shortControlCharEscapes[ch] = "\\" .. i - longControlCharEscapes[ch] = string.format("\\%03d", i) + longControlCharEscapes[ch] = fmt("\\%03d", i) end end local function escape(str) - return (str:gsub("\\", "\\\\"): - gsub("(%c)%f[0-9]", longControlCharEscapes): - gsub("%c", shortControlCharEscapes)) + return (gsub(gsub(gsub(str, "\\", "\\\\"), + "(%c)%f[0-9]", longControlCharEscapes), + "%c", shortControlCharEscapes)) end local function isIdentifier(str) @@ -107,59 +112,45 @@ local function sortKeys(a, b) return (a) < (b) end - local dta, dtb = defaultTypeOrders[ta], defaultTypeOrders[tb] - - if dta and dtb then return defaultTypeOrders[ta] < defaultTypeOrders[tb] - elseif dta then return true - elseif dtb then return false - end + local dta = defaultTypeOrders[ta] or 100 + local dtb = defaultTypeOrders[tb] or 100 - return ta < tb + return dta == dtb and ta < tb or dta < dtb end +local function getKeys(t) - -local function getSequenceLength(t) - local len = 1 - local v = rawget(t, len) - while v ~= nil do - len = len + 1 - v = rawget(t, len) + local seqLen = 1 + while rawget(t, seqLen) ~= nil do + seqLen = seqLen + 1 end - return len - 1 -end + seqLen = seqLen - 1 -local function getNonSequentialKeys(t) - local keys, keysLength = {}, 0 - local sequenceLength = getSequenceLength(t) - for k, _ in rawpairs(t) do - if not isSequenceKey(k, sequenceLength) then - keysLength = keysLength + 1 - keys[keysLength] = k + local keys, keysLen = {}, 0 + for k in rawpairs(t) do + if not isSequenceKey(k, seqLen) then + keysLen = keysLen + 1 + keys[keysLen] = k end end table.sort(keys, sortKeys) - return keys, keysLength, sequenceLength + return keys, keysLen, seqLen end -local function countTableAppearances(t, tableAppearances) - tableAppearances = tableAppearances or {} - - if type(t) == "table" then - if not tableAppearances[t] then - tableAppearances[t] = 1 - for k, v in rawpairs(t) do - countTableAppearances(k, tableAppearances) - countTableAppearances(v, tableAppearances) - end - countTableAppearances(getmetatable(t), tableAppearances) +local function countCycles(x, cycles) + if type(x) == "table" then + if cycles[x] then + cycles[x] = cycles[x] + 1 else - tableAppearances[t] = tableAppearances[t] + 1 + cycles[x] = 1 + for k, v in rawpairs(x) do + countCycles(k, cycles) + countCycles(v, cycles) + end + countCycles(getmetatable(x), cycles) end end - - return tableAppearances end local function makePath(path, a, b) @@ -202,7 +193,10 @@ local function processRecursive(process, return processed end - +local function puts(buf, str) + buf.n = buf.n + 1 + buf[buf.n] = str +end @@ -219,118 +213,85 @@ local Inspector = {} local Inspector_mt = { __index = Inspector } -function Inspector:puts(a, b, c, d, e) - local buffer = self.buffer - local len = #buffer - buffer[len + 1] = a - buffer[len + 2] = b - buffer[len + 3] = c - buffer[len + 4] = d - buffer[len + 5] = e -end - -function Inspector:down(f) - self.level = self.level + 1 - f() - self.level = self.level - 1 -end - -function Inspector:tabify() - self:puts(self.newline, - string.rep(self.indent, self.level)) -end - -function Inspector:alreadyVisited(v) - return self.ids[v] ~= nil +local function tabify(inspector) + puts(inspector.buf, inspector.newline .. rep(inspector.indent, inspector.level)) end function Inspector:getId(v) local id = self.ids[v] + local ids = self.ids if not id then local tv = type(v) - id = (self.maxIds[tv] or 0) + 1 - self.maxIds[tv] = id - self.ids[v] = id + id = (ids[tv] or 0) + 1 + ids[v], ids[tv] = id, id end return tostring(id) end - -function Inspector:putValue(_) -end - -function Inspector:putKey(k) - if isIdentifier(k) then - self:puts(k) - return - end - self:puts("[") - self:putValue(k) - self:puts("]") -end - -function Inspector:putTable(t) - if t == inspect.KEY or t == inspect.METATABLE then - self:puts(tostring(t)) - elseif self:alreadyVisited(t) then - self:puts('') - elseif self.level >= self.depth then - self:puts('{...}') - else - if self.tableAppearances[t] > 1 then self:puts('<', self:getId(t), '>') end - - local nonSequentialKeys, nonSequentialKeysLength, sequenceLength = getNonSequentialKeys(t) - local mt = getmetatable(t) - - self:puts('{') - self:down(function() - local count = 0 - for i = 1, sequenceLength do - if count > 0 then self:puts(',') end - self:puts(' ') - self:putValue(t[i]) - count = count + 1 - end - - for i = 1, nonSequentialKeysLength do - local k = nonSequentialKeys[i] - if count > 0 then self:puts(',') end - self:tabify() - self:putKey(k) - self:puts(' = ') - self:putValue(t[k]) - count = count + 1 +function Inspector:putValue(v) + local buf = self.buf + local tv = type(v) + if tv == 'string' then + puts(buf, smartQuote(escape(v))) + elseif tv == 'number' or tv == 'boolean' or tv == 'nil' or + tv == 'cdata' or tv == 'ctype' then + puts(buf, tostring(v)) + elseif tv == 'table' and not self.ids[v] then + local t = v + + if t == inspect.KEY or t == inspect.METATABLE then + puts(buf, tostring(t)) + elseif self.level >= self.depth then + puts(buf, '{...}') + else + if self.cycles[t] > 1 then puts(buf, fmt('<%d>', self:getId(t))) end + + local keys, keysLen, seqLen = getKeys(t) + + puts(buf, '{') + self.level = self.level + 1 + + for i = 1, seqLen + keysLen do + if i > 1 then puts(buf, ',') end + if i <= seqLen then + puts(buf, ' ') + self:putValue(t[i]) + else + local k = keys[i - seqLen] + tabify(self) + if isIdentifier(k) then + puts(buf, k) + else + puts(buf, "[") + self:putValue(k) + puts(buf, "]") + end + puts(buf, ' = ') + self:putValue(t[k]) + end end + local mt = getmetatable(t) if type(mt) == 'table' then - if count > 0 then self:puts(',') end - self:tabify() - self:puts(' = ') + if seqLen + keysLen > 0 then puts(buf, ',') end + tabify(self) + puts(buf, ' = ') self:putValue(mt) end - end) - if nonSequentialKeysLength > 0 or type(mt) == 'table' then - self:tabify() - elseif sequenceLength > 0 then - self:puts(' ') - end + self.level = self.level - 1 - self:puts('}') - end -end + if keysLen > 0 or type(mt) == 'table' then + tabify(self) + elseif seqLen > 0 then + puts(buf, ' ') + end + + puts(buf, '}') + end -function Inspector:putValue(v) - local tv = type(v) - if tv == 'string' then - self:puts(smartQuote(escape(v))) - elseif tv == 'number' or tv == 'boolean' or tv == 'nil' or - tv == 'cdata' or tv == 'ctype' then - self:puts(tostring(v)) - elseif tv == 'table' then - self:putTable(v) else - self:puts('<', tv, ' ', self:getId(v), '>') + puts(buf, fmt('<%s %d>', tv, self:getId(v))) end end @@ -349,20 +310,22 @@ function inspect.inspect(root, options) root = processRecursive(process, root, {}, {}) end + local cycles = {} + countCycles(root, cycles) + local inspector = setmetatable({ + buf = { n = 0 }, + ids = {}, + cycles = cycles, depth = depth, level = 0, - buffer = {}, - ids = {}, - maxIds = {}, newline = newline, indent = indent, - tableAppearances = countTableAppearances(root), }, Inspector_mt) inspector:putValue(root) - return table.concat(inspector.buffer) + return table.concat(inspector.buf) end setmetatable(inspect, { diff --git a/inspect.tl b/inspect.tl index 23efa63..7ac64c8 100644 --- a/inspect.tl +++ b/inspect.tl @@ -48,6 +48,11 @@ inspect.KEY = setmetatable({}, {__tostring = function(): string return 'in inspect.METATABLE = setmetatable({}, {__tostring = function(): string return 'inspect.METATABLE' end}) local tostring = tostring +local rep = string.rep +local match = string.match +local char = string.char +local gsub = string.gsub +local fmt = string.format local function rawpairs(t: table): function, table, nil return next, t, nil @@ -56,10 +61,10 @@ end -- Apostrophizes the string if it has quotes, but not aphostrophes -- Otherwise, it returns a regular quoted string local function smartQuote(str: string): string - if str:match('"') and not str:match("'") then + if match(str, '"') and not match(str, "'") then return "'" .. str .. "'" end - return '"' .. str:gsub('"', '\\"') .. '"' + return '"' .. gsub(str, '"', '\\"') .. '"' end -- \a => '\\a', \0 => '\\0', 31 => '\31' @@ -69,17 +74,17 @@ local shortControlCharEscapes: {string:string} = { } local longControlCharEscapes: {string:string} = {["\127"]="\127"} -- \a => nil, \0 => \000, 31 => \031 for i=0, 31 do - local ch: string = string.char(i) + local ch: string = char(i) if not shortControlCharEscapes[ch] then shortControlCharEscapes[ch] = "\\"..i - longControlCharEscapes[ch] = string.format("\\%03d", i) + longControlCharEscapes[ch] = fmt("\\%03d", i) end end local function escape(str: string): string - return (str:gsub("\\", "\\\\") - :gsub("(%c)%f[0-9]", longControlCharEscapes) - :gsub("%c", shortControlCharEscapes)) + return (gsub(gsub(gsub(str,"\\", "\\\\"), + "(%c)%f[0-9]", longControlCharEscapes), + "%c", shortControlCharEscapes)) end local function isIdentifier(str: any): boolean @@ -107,59 +112,45 @@ local function sortKeys(a:any, b:any): boolean return (a as string) < (b as string) end - local dta, dtb: integer, integer = defaultTypeOrders[ta], defaultTypeOrders[tb] - -- Two default types are compared according to the defaultTypeOrders table - if dta and dtb then return defaultTypeOrders[ta] < defaultTypeOrders[tb] - elseif dta then return true -- default types before custom ones - elseif dtb then return false -- custom types after default ones - end - - -- custom types are sorted out alphabetically - return ta < tb + local dta: integer = defaultTypeOrders[ta] or 100 + local dtb: integer = defaultTypeOrders[tb] or 100 + -- Default types are compared according to defaultTypeOrders + -- Custom types are compared alphabetically + return dta == dtb and ta < tb or dta < dtb end --- For implementation reasons, the behavior of rawlen & # is "undefined" when --- tables aren't pure sequences. So we implement our own # operator. -local function getSequenceLength(t: table): integer - local len: integer = 1 - local v: any = rawget(t, len) - while v ~= nil do - len = len + 1 - v = rawget(t,len) +local function getKeys(t: table): {any}, integer, integer + -- seqLen counts the "array-like" keys + local seqLen: integer = 1 + while rawget(t, seqLen) ~= nil do + seqLen = seqLen + 1 end - return len - 1 -end + seqLen = seqLen - 1 -local function getNonSequentialKeys(t: table): {any}, integer, integer - local keys, keysLength: {any}, integer = {}, 0 - local sequenceLength: integer = getSequenceLength(t) - for k,_ in rawpairs(t) do - if not isSequenceKey(k, sequenceLength) then - keysLength = keysLength + 1 - keys[keysLength] = k + local keys, keysLen: {any}, integer = {}, 0 + for k in rawpairs(t) do + if not isSequenceKey(k, seqLen) then + keysLen = keysLen + 1 + keys[keysLen] = k end end table.sort(keys, sortKeys) - return keys, keysLength, sequenceLength + return keys, keysLen, seqLen end -local function countTableAppearances(t: any, tableAppearances: {any:integer}): {any:integer} - tableAppearances = tableAppearances or {} - - if t is table then - if not tableAppearances[t] then - tableAppearances[t] = 1 - for k,v in rawpairs(t) do - countTableAppearances(k, tableAppearances) - countTableAppearances(v, tableAppearances) - end - countTableAppearances(getmetatable(t), tableAppearances) +local function countCycles(x: any, cycles: {any:integer}): nil + if x is table then + if cycles[x] then + cycles[x] = cycles[x] + 1 else - tableAppearances[t] = tableAppearances[t] + 1 + cycles[x] = 1 + for k,v in rawpairs(x) do + countCycles(k, cycles) + countCycles(v, cycles) + end + countCycles(getmetatable(x), cycles) end end - - return tableAppearances end local function makePath(path: {any}, a: any, b: any): {any} @@ -202,135 +193,105 @@ local function processRecursive(process: inspect.ProcessFunction, return processed end - +local function puts(buf: table, str:string): nil + buf.n = buf.n as integer + 1 + buf[buf.n as integer] = str +end ------------------------------------------------------------------- local type Inspector = record + buf: table depth: integer level: integer - buffer: {string} ids: {any:integer} - maxIds: {any:integer} newline: string indent: string - tableAppearances: {table: integer} + cycles: {table: integer} + puts: function(string) end local Inspector_mt = {__index = Inspector} -function Inspector:puts(a:string, b:string, c:string, d:string, e:string): nil - local buffer: {string} = self.buffer - local len: integer = #buffer - buffer[len+1] = a - buffer[len+2] = b - buffer[len+3] = c - buffer[len+4] = d - buffer[len+5] = e -end - -function Inspector:down(f: function()): nil - self.level = self.level + 1 - f() - self.level = self.level - 1 -end - -function Inspector:tabify(): nil - self:puts(self.newline, - string.rep(self.indent, self.level)) -end - -function Inspector:alreadyVisited(v: any): boolean - return self.ids[v] ~= nil +local function tabify(inspector: Inspector) + puts(inspector.buf, inspector.newline .. rep(inspector.indent, inspector.level)) end function Inspector:getId(v: any): string local id: integer = self.ids[v] + local ids = self.ids if not id then local tv: string = type(v) - id = (self.maxIds[tv] or 0) + 1 - self.maxIds[tv] = id - self.ids[v] = id + id = (ids[tv] or 0) + 1 + ids[v], ids[tv] = id, id end return tostring(id) end --- dummy function; defined later -function Inspector:putValue(_: any):nil -end - -function Inspector:putKey(k: any): nil - if isIdentifier(k) then - self:puts(k as string) - return - end - self:puts("[") - self:putValue(k) - self:puts("]") -end - -function Inspector:putTable(t: table): nil - if t == inspect.KEY or t == inspect.METATABLE then - self:puts(tostring(t)) - elseif self:alreadyVisited(t) then - self:puts('
') - elseif self.level >= self.depth then - self:puts('{...}') - else - if self.tableAppearances[t] > 1 then self:puts('<', self:getId(t), '>') end - - local nonSequentialKeys, nonSequentialKeysLength, sequenceLength = getNonSequentialKeys(t) - local mt = getmetatable(t) - - self:puts('{') - self:down(function() - local count = 0 - for i=1, sequenceLength do - if count > 0 then self:puts(',') end - self:puts(' ') - self:putValue(t[i]) - count = count + 1 - end - - for i=1, nonSequentialKeysLength do - local k = nonSequentialKeys[i] - if count > 0 then self:puts(',') end - self:tabify() - self:putKey(k) - self:puts(' = ') - self:putValue(t[k]) - count = count + 1 +function Inspector:putValue(v: any) + local buf = self.buf + local tv: string = type(v) + if tv == 'string' then + puts(buf, smartQuote(escape(v as string))) + elseif tv == 'number' or tv == 'boolean' or tv == 'nil' or + tv == 'cdata' or tv == 'ctype' then + puts(buf, tostring(v as number)) + elseif tv == 'table' and not self.ids[v] then + local t = v as table + + if t == inspect.KEY or t == inspect.METATABLE then + puts(buf, tostring(t)) + elseif self.level >= self.depth then + puts(buf, '{...}') + else + if self.cycles[t] > 1 then puts(buf, fmt('<%d>', self:getId(t))) end + + local keys, keysLen, seqLen = getKeys(t) + + puts(buf, '{') + self.level = self.level + 1 + + for i = 1, seqLen + keysLen do + if i > 1 then puts(buf, ',') end + if i <= seqLen then + puts(buf, ' ') + self:putValue(t[i]) + else + local k = keys[i - seqLen] + tabify(self) + if isIdentifier(k) then + puts(buf, k as string) + else + puts(buf, "[") + self:putValue(k) + puts(buf, "]") + end + puts(buf, ' = ') + self:putValue(t[k]) + end end + local mt = getmetatable(t) if type(mt) == 'table' then - if count > 0 then self:puts(',') end - self:tabify() - self:puts(' = ') - self:putValue(mt) + if seqLen + keysLen > 0 then puts(buf, ',') end + tabify(self) + puts(buf, ' = ') + self:putValue(mt) end - end) - if nonSequentialKeysLength > 0 or type(mt) == 'table' then -- result is multi-lined. Justify closing } - self:tabify() - elseif sequenceLength > 0 then -- array tables have one extra space before closing } - self:puts(' ') - end + self.level = self.level - 1 - self:puts('}') - end -end + if keysLen > 0 or type(mt) == 'table' then -- result is multi-lined. Justify closing } + tabify(self) + elseif seqLen > 0 then -- array tables have one extra space before closing } + puts(buf, ' ') + end + + puts(buf, '}') + end -function Inspector:putValue(v: any) - local tv: string = type(v) - if tv == 'string' then - self:puts(smartQuote(escape(v as string))) - elseif tv == 'number' or tv == 'boolean' or tv == 'nil' or - tv == 'cdata' or tv == 'ctype' then - self:puts(tostring(v as number)) - elseif tv == 'table' then - self:putTable(v as table) else - self:puts('<', tv, ' ', self:getId(v), '>') + puts(buf, fmt('<%s %d>', tv, self:getId(v))) end end @@ -349,20 +310,22 @@ function inspect.inspect(root: any, options: inspect.Options): string root = processRecursive(process, root, {}, {}) end + local cycles = {} + countCycles(root, cycles) + local inspector = setmetatable({ + buf = { n = 0 }, + ids = {}, + cycles = cycles, depth = depth, level = 0, - buffer = {}, - ids = {}, - maxIds = {}, newline = newline, indent = indent, - tableAppearances = countTableAppearances(root) } as Inspector, Inspector_mt) inspector:putValue(root) - return table.concat(inspector.buffer) + return table.concat(inspector.buf as {string}) end setmetatable(inspect, { diff --git a/perf.lua b/perf.lua new file mode 100644 index 0000000..818ef85 --- /dev/null +++ b/perf.lua @@ -0,0 +1,112 @@ +local inspect = require 'inspect' + +local skip_headers = ... + +local N=100000 + +local results = {} + +local time = function(name, n, f) + local clock = os.clock + + collectgarbage() + collectgarbage() + collectgarbage() + + local startTime = clock() + + for i=0,n do f() end + + local duration = clock() - startTime + + results[#results + 1] = { name, duration } +end + +------------------- + +time('nil', N, function() + inspect(nil) +end) + +time('string', N, function() + inspect("hello") +end) + +local e={} +time('empty', N, function() + inspect(e) +end) + +local seq={1,2,3,4} +time('sequence', N, function() + inspect(seq) +end) + +local record={a=1, b=2, c=3} +time('record', N, function() + inspect(record) +end) + +local hybrid={1, 2, 3, a=1, b=2, c=3} +time('hybrid', N, function() + inspect(hybrid) +end) + +local recursive = {} +recursive.x = recursive +time('recursive', N, function() + inspect(recursive) +end) + +local with_meta=setmetatable({}, + { __tostring = function() return "s" end }) +time('meta', N, function() + inspect(with_meta) +end) + +local process_options = { + process = function(i,p) return "p" end +} +time('process', N, function() + inspect(seq, process_options) +end) + +local complex = { + a = 1, + true, + print, + [print] = print, + [{}] = { {}, 3, b = {x = 42} } +} +complex.x = complex +setmetatable(complex, complex) +time('complex', N, function() + inspect(complex) +end) + +local big = {} +for i = 1,1000 do + big[i] = i +end +for i = 1,1000 do + big["a" .. i] = 1 +end +time('big', N/100, function() + inspect(big) +end) + +------ + +if not skip_headers then + for i,r in ipairs(results) do + if i > 1 then io.write(",") end + io.write(r[1]) + end + io.write("\n") +end + +for i,r in ipairs(results) do + if i > 1 then io.write(",") end + io.write(r[2]) +end +io.write("\n")