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")