From 8429ddc74864c00743b6fb84454e14dd4bcc0284 Mon Sep 17 00:00:00 2001 From: MoreThanTom Date: Mon, 5 Aug 2019 22:04:12 +0100 Subject: [PATCH 1/7] First implementation of pretty encode Any encoders with the same indent share a cache. Only one assignment is made on depth change. `space` is equivalent to the third argument of [javascript's JSON.stringify](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) --- src/lunajson/encoder.lua | 78 +++++++++++++++++++++++++++++++--------- 1 file changed, 61 insertions(+), 17 deletions(-) diff --git a/src/lunajson/encoder.lua b/src/lunajson/encoder.lua index 606767a..a1fa468 100644 --- a/src/lunajson/encoder.lua +++ b/src/lunajson/encoder.lua @@ -1,11 +1,26 @@ local error = error -local byte, find, format, gsub, match = string.byte, string.find, string.format, string.gsub, string.match +local byte, find, format, gsub, match, rep = string.byte, string.find, string.format, string.gsub, string.match, string.rep local concat = table.concat local tostring = tostring local pairs, type = pairs, type local setmetatable = setmetatable local huge, tiny = 1/0, -1/0 +local set_cache = setmetatable({}, { __weak = "k" }) +local basic = { + start_array = '[', + end_array = ']', + start_object = '{', + end_object = '}', + split_element = ',' +} +local delim_tmpls = { + start_array = '[\n%s%%s', + end_array = '\n%%s]', + start_object = '{\n%s%%s', + end_object = '\n%%s}', + split_element = ',\n%s%%s' +} local f_string_esc_pat if _VERSION == "Lua 5.1" then -- use the cluttered pattern because lua 5.1 does not handle \0 in a pattern correctly @@ -14,13 +29,13 @@ else f_string_esc_pat = '[\0-\31"\\]' end -local _ENV = nil +local one_space = " " - -local function newencoder() +local function newencoder(space) + local _ENV local v, nullv local i, builder, visited - + local colon = ':' local function f_tostring(v) builder[i] = tostring(v) i = i+1 @@ -86,7 +101,6 @@ local function newencoder() builder[i+2] = '"' i = i+3 end - local function f_table(o) if visited[o] then error("loop detected") @@ -95,22 +109,22 @@ local function newencoder() local tmp = o[0] if type(tmp) == 'number' then -- arraylen available - builder[i] = '[' + builder[i] = start_array i = i+1 for j = 1, tmp do doencode(o[j]) - builder[i] = ',' + builder[i] = split_element i = i+1 end if tmp > 0 then i = i-1 end - builder[i] = ']' + builder[i] = end_array else tmp = o[1] if tmp ~= nil then -- detected as array - builder[i] = '[' + builder[i] = start_array i = i+1 local j = 2 repeat @@ -120,13 +134,13 @@ local function newencoder() break end j = j+1 - builder[i] = ',' + builder[i] = split_element i = i+1 until false - builder[i] = ']' + builder[i] = end_array else -- detected as object - builder[i] = '{' + builder[i] = start_object i = i+1 local tmp = i for k, v in pairs(o) do @@ -134,23 +148,53 @@ local function newencoder() error("non-string key") end f_string(k) - builder[i] = ':' + builder[i] = colon i = i+1 doencode(v) - builder[i] = ',' + builder[i] = split_element i = i+1 end if i > tmp then i = i-1 end - builder[i] = '}' + builder[i] = end_object end end i = i+1 visited[o] = nil end - + if type(space) == "number" then space = rep(one_space, space) end + if type(space) ~= "string" or space == "" then _ENV = basic else + colon = colon .. " " + local f = f_table + local delim_set = set_cache[space] + if not delim_set then + delim_set = {} + set_cache[space] = {} + end + for k, v in pairs(delim_tmpls) do + delim_set[k] = format(v, space) + end + local depth = -1 + function f_table(o) + depth = depth + 1 + local delims = delim_set[depth] + if not delims then + delims = setmetatable({}, { + __index = function(t, k) + t[k] = format(delim_set[k], rep(space, depth)) + return t[k] + end + }) + delim_set[depth] = delims + end + _ENV = delims + f(o) + depth = depth - 1 + _ENV = delim_set[depth] + end + end local dispatcher = { boolean = f_tostring, number = f_number, From a62dd998d41cfb21472054b9adcb725bb3ed5c99 Mon Sep 17 00:00:00 2001 From: MoreThanTom Date: Tue, 6 Aug 2019 20:37:10 +0100 Subject: [PATCH 2/7] Lua 5.1 compatability --- src/lunajson/encoder.lua | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/lunajson/encoder.lua b/src/lunajson/encoder.lua index a1fa468..57b8b45 100644 --- a/src/lunajson/encoder.lua +++ b/src/lunajson/encoder.lua @@ -22,9 +22,14 @@ local delim_tmpls = { split_element = ',\n%s%%s' } local f_string_esc_pat +local setenv if _VERSION == "Lua 5.1" then -- use the cluttered pattern because lua 5.1 does not handle \0 in a pattern correctly f_string_esc_pat = '[^ -!#-[%]^-\255]' + local setfenv = setfenv + function setenv(ENV) + setfenv(2, ENV) + end else f_string_esc_pat = '[\0-\31"\\]' end @@ -33,6 +38,7 @@ local one_space = " " local function newencoder(space) local _ENV + local setenv = setenv or function(ENV) _ENV = ENV end local v, nullv local i, builder, visited local colon = ':' @@ -165,7 +171,7 @@ local function newencoder(space) visited[o] = nil end if type(space) == "number" then space = rep(one_space, space) end - if type(space) ~= "string" or space == "" then _ENV = basic else + if type(space) ~= "string" or space == "" then setenv(basic) else colon = colon .. " " local f = f_table local delim_set = set_cache[space] @@ -189,10 +195,10 @@ local function newencoder(space) }) delim_set[depth] = delims end - _ENV = delims + setenv(delims) f(o) depth = depth - 1 - _ENV = delim_set[depth] + setenv(delim_set[depth]) end end local dispatcher = { From 353cbebb834f88c12e9b5336de8af92fe5a3462a Mon Sep 17 00:00:00 2001 From: MoreThanTom Date: Tue, 6 Aug 2019 20:48:00 +0100 Subject: [PATCH 3/7] Avoid spacing in empty objects --- src/lunajson/encoder.lua | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/lunajson/encoder.lua b/src/lunajson/encoder.lua index 57b8b45..6370faf 100644 --- a/src/lunajson/encoder.lua +++ b/src/lunajson/encoder.lua @@ -115,18 +115,21 @@ local function newencoder(space) local tmp = o[0] if type(tmp) == 'number' then -- arraylen available - builder[i] = start_array - i = i+1 - for j = 1, tmp do - doencode(o[j]) - builder[i] = split_element + if tmp == 0 then + builder[i] = '[]' + else + builder[i] = start_array i = i+1 + for j = 1, tmp do + doencode(o[j]) + builder[i] = split_element + i = i+1 + end + if tmp > 0 then + i = i-1 + end + builder[i] = end_array end - if tmp > 0 then - i = i-1 - end - builder[i] = end_array - else tmp = o[1] if tmp ~= nil then -- detected as array @@ -160,10 +163,12 @@ local function newencoder(space) builder[i] = split_element i = i+1 end + i = i-1 if i > tmp then - i = i-1 + builder[i] = end_object + else + builder[i] = '{}' end - builder[i] = end_object end end From 9488f33681297972c9168744e5d1b7f6d50dd141 Mon Sep 17 00:00:00 2001 From: MoreThanTom Date: Wed, 7 Aug 2019 11:40:06 +0100 Subject: [PATCH 4/7] Use a local variable instead of ENV for delimiters Turns out using the environment has zero performance benefit in any version of lua, so it just serves to make the code less readable --- src/lunajson/encoder.lua | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/src/lunajson/encoder.lua b/src/lunajson/encoder.lua index 6370faf..7f462f3 100644 --- a/src/lunajson/encoder.lua +++ b/src/lunajson/encoder.lua @@ -22,14 +22,9 @@ local delim_tmpls = { split_element = ',\n%s%%s' } local f_string_esc_pat -local setenv if _VERSION == "Lua 5.1" then -- use the cluttered pattern because lua 5.1 does not handle \0 in a pattern correctly f_string_esc_pat = '[^ -!#-[%]^-\255]' - local setfenv = setfenv - function setenv(ENV) - setfenv(2, ENV) - end else f_string_esc_pat = '[\0-\31"\\]' end @@ -37,8 +32,7 @@ end local one_space = " " local function newencoder(space) - local _ENV - local setenv = setenv or function(ENV) _ENV = ENV end + local active_delims local v, nullv local i, builder, visited local colon = ':' @@ -118,22 +112,22 @@ local function newencoder(space) if tmp == 0 then builder[i] = '[]' else - builder[i] = start_array + builder[i] = active_delims.start_array i = i+1 for j = 1, tmp do doencode(o[j]) - builder[i] = split_element + builder[i] = active_delims.split_element i = i+1 end if tmp > 0 then i = i-1 end - builder[i] = end_array + builder[i] = active_delims.end_array end else tmp = o[1] if tmp ~= nil then -- detected as array - builder[i] = start_array + builder[i] = active_delims.start_array i = i+1 local j = 2 repeat @@ -143,13 +137,13 @@ local function newencoder(space) break end j = j+1 - builder[i] = split_element + builder[i] = active_delims.split_element i = i+1 until false - builder[i] = end_array + builder[i] = active_delims.end_array else -- detected as object - builder[i] = start_object + builder[i] = active_delims.start_object i = i+1 local tmp = i for k, v in pairs(o) do @@ -160,12 +154,12 @@ local function newencoder(space) builder[i] = colon i = i+1 doencode(v) - builder[i] = split_element + builder[i] = active_delims.split_element i = i+1 end i = i-1 if i > tmp then - builder[i] = end_object + builder[i] = active_delims.end_object else builder[i] = '{}' end @@ -176,7 +170,7 @@ local function newencoder(space) visited[o] = nil end if type(space) == "number" then space = rep(one_space, space) end - if type(space) ~= "string" or space == "" then setenv(basic) else + if type(space) ~= "string" or space == "" then active_delims = basic else colon = colon .. " " local f = f_table local delim_set = set_cache[space] @@ -200,10 +194,10 @@ local function newencoder(space) }) delim_set[depth] = delims end - setenv(delims) + active_delims = delims f(o) depth = depth - 1 - setenv(delim_set[depth]) + active_delims = delim_set[depth] end end local dispatcher = { From 80805ad406626bec6f28494d40054d8ad63cbc0d Mon Sep 17 00:00:00 2001 From: MoreThanTom Date: Wed, 7 Aug 2019 11:41:47 +0100 Subject: [PATCH 5/7] Clearer variable name --- src/lunajson/encoder.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lunajson/encoder.lua b/src/lunajson/encoder.lua index 7f462f3..7e4258d 100644 --- a/src/lunajson/encoder.lua +++ b/src/lunajson/encoder.lua @@ -107,19 +107,19 @@ local function newencoder(space) end visited[o] = true - local tmp = o[0] - if type(tmp) == 'number' then -- arraylen available - if tmp == 0 then + local arraylen = o[0] + if type(arraylen) == 'number' then -- arraylen available + if arraylen == 0 then builder[i] = '[]' else builder[i] = active_delims.start_array i = i+1 - for j = 1, tmp do + for j = 1, arraylen do doencode(o[j]) builder[i] = active_delims.split_element i = i+1 end - if tmp > 0 then + if arraylen > 0 then i = i-1 end builder[i] = active_delims.end_array From f0dc1f6cb867a86ceb29741595ff82680d67810b Mon Sep 17 00:00:00 2001 From: MoreThanTom Date: Wed, 7 Aug 2019 12:00:49 +0100 Subject: [PATCH 6/7] Move the space parameter to the encode function This brings the interface more in line with the rest of the library --- src/lunajson/encoder.lua | 85 ++++++++++++++++++++++------------------ 1 file changed, 46 insertions(+), 39 deletions(-) diff --git a/src/lunajson/encoder.lua b/src/lunajson/encoder.lua index 7e4258d..7a7bc07 100644 --- a/src/lunajson/encoder.lua +++ b/src/lunajson/encoder.lua @@ -31,11 +31,11 @@ end local one_space = " " -local function newencoder(space) +local function newencoder() local active_delims - local v, nullv + local v, nullv, colon local i, builder, visited - local colon = ':' + local function f_tostring(v) builder[i] = tostring(v) i = i+1 @@ -101,7 +101,7 @@ local function newencoder(space) builder[i+2] = '"' i = i+3 end - local function f_table(o) + local function basic_f_table(o) if visited[o] then error("loop detected") end @@ -169,42 +169,12 @@ local function newencoder(space) i = i+1 visited[o] = nil end - if type(space) == "number" then space = rep(one_space, space) end - if type(space) ~= "string" or space == "" then active_delims = basic else - colon = colon .. " " - local f = f_table - local delim_set = set_cache[space] - if not delim_set then - delim_set = {} - set_cache[space] = {} - end - for k, v in pairs(delim_tmpls) do - delim_set[k] = format(v, space) - end - local depth = -1 - function f_table(o) - depth = depth + 1 - local delims = delim_set[depth] - if not delims then - delims = setmetatable({}, { - __index = function(t, k) - t[k] = format(delim_set[k], rep(space, depth)) - return t[k] - end - }) - delim_set[depth] = delims - end - active_delims = delims - f(o) - depth = depth - 1 - active_delims = delim_set[depth] - end - end + local f_table local dispatcher = { boolean = f_tostring, number = f_number, string = f_string, - table = f_table, + table = function(...) return f_table(...) end, __index = function() error("invalid type value") end @@ -219,10 +189,47 @@ local function newencoder(space) end return dispatcher[type(v)](v) end - - local function encode(v_, nullv_) - v, nullv = v_, nullv_ + local delim_set, space, depth + local delims_meta = {} + function delims_meta:__index(k) + local s = format(delim_set[k], rep(space, depth)) + self[k] = s + return s + end + local function spaced_f_table(o) + depth = depth + 1 + local delims = delim_set[depth] + if not delims then + delims = setmetatable({}, delims_meta) + delim_set[depth] = delims + end + active_delims = delims + basic_f_table(o) + depth = depth - 1 + active_delims = delim_set[depth] + end + local function encode(v_, nullv_, _space) + v, nullv, space = v_, nullv_, _space i, builder, visited = 1, {}, {} + + if type(space) == "number" then space = rep(one_space, space) end + if type(space) ~= "string" or space == "" then + active_delims = basic + f_table = basic_f_table + colon = ":" + else + colon = ": " + depth = -1 + f_table = spaced_f_table + delim_set = set_cache[space] + if not delim_set then + delim_set = {} + set_cache[space] = {} + for k, v in pairs(delim_tmpls) do + delim_set[k] = format(v, space) + end + end + end doencode(v) return concat(builder) From 579ee3a158784fcaa6d55a0baf8906ffc215db6f Mon Sep 17 00:00:00 2001 From: MoreThanTom Date: Wed, 7 Aug 2019 15:17:36 +0100 Subject: [PATCH 7/7] Restore empty _ENV This doesn't really do anything for consumers, however it is valuable for ensuring variables are made local --- src/lunajson/encoder.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lunajson/encoder.lua b/src/lunajson/encoder.lua index 7a7bc07..e221c41 100644 --- a/src/lunajson/encoder.lua +++ b/src/lunajson/encoder.lua @@ -29,6 +29,7 @@ else f_string_esc_pat = '[\0-\31"\\]' end +local _ENV = nil local one_space = " " local function newencoder()