diff options
Diffstat (limited to 'daemon/lua/krprint.lua')
-rw-r--r-- | daemon/lua/krprint.lua | 340 |
1 files changed, 340 insertions, 0 deletions
diff --git a/daemon/lua/krprint.lua b/daemon/lua/krprint.lua new file mode 100644 index 0000000..dd25a9b --- /dev/null +++ b/daemon/lua/krprint.lua @@ -0,0 +1,340 @@ +-- SPDX-License-Identifier: GPL-3.0-or-later +local base_class = { + cur_indent = 0, +} + +-- shared constructor: use as serializer_class:new() +function base_class.new(class, on_unrepresentable) + on_unrepresentable = on_unrepresentable or 'comment' + if on_unrepresentable ~= 'comment' + and on_unrepresentable ~= 'error' then + error('unsupported val2expr on_unrepresentable option ' + .. tostring(on_unrepresentable)) + end + local inst = {} + inst.on_unrepresentable = on_unrepresentable + inst.done = {} + inst.tab_key_path = {} + setmetatable(inst, class.__inst_mt) + return inst +end + +-- format comment with leading/ending whitespace if needed +function base_class.format_note(_, note, ws_prefix, ws_suffix) + if note == nil then + return '' + else + return string.format('%s--[[ %s ]]%s', + ws_prefix or '', note, ws_suffix or '') + end +end + +function base_class.indent_head(self) + return string.rep(' ', self.cur_indent) +end + +function base_class.indent_inc(self) + self.cur_indent = self.cur_indent + self.indent_step +end + +function base_class.indent_dec(self) + self.cur_indent = self.cur_indent - self.indent_step +end + +function base_class._fallback(self, val) + if self.on_unrepresentable == 'comment' then + return 'nil', string.format('missing %s', val) + elseif self.on_unrepresentable == 'error' then + local key_path_msg + if #self.tab_key_path > 0 then + local str_key_path = {} + for _, key in ipairs(self.tab_key_path) do + table.insert(str_key_path, + string.format('%s %s', type(key), self:string(tostring(key)))) + end + local key_path = '[' .. table.concat(str_key_path, '][') .. ']' + key_path_msg = string.format(' (found at [%s])', key_path) + else + key_path_msg = '' + end + error(string.format('cannot serialize type %s%s', type(val), key_path_msg), 2) + end +end + +function base_class.val2expr(self, val) + local val_type = type(val) + local val_repr = self[val_type] + if val_repr then + return val_repr(self, val) + else + return self:_fallback(val) + end +end + +-- "nil" is a Lua keyword so assignment below is workaround to create +-- function base_class.nil(self, val) +base_class['nil'] = function(_, val) + assert(type(val) == 'nil') + return 'nil' +end + +function base_class.number(_, val) + assert(type(val) == 'number') + if val == math.huge then + return 'math.huge' + elseif val == -math.huge then + return '-math.huge' + elseif tostring(val) == 'nan' then + return 'tonumber(\'nan\')' + else + return string.format("%.60f", val) + end +end + +function base_class.char_is_printable(_, c) + -- ASCII (from space to ~) and not ' or \ + return (c >= 0x20 and c < 0x7f) + and c ~= 0x27 and c ~= 0x5C +end + +function base_class.string(self, val) + assert(type(val) == 'string') + local chars = {'\''} + for i = 1, #val do + local c = string.byte(val, i) + if self:char_is_printable(c) then + table.insert(chars, string.char(c)) + else + table.insert(chars, string.format('\\%03d', c)) + end + end + table.insert(chars, '\'') + return table.concat(chars) +end + +function base_class.boolean(_, val) + assert(type(val) == 'boolean') + return tostring(val) +end + +local function ordered_iter(unordered_tt) + local keys = {} + for k in pairs(unordered_tt) do + table.insert(keys, k) + end + table.sort(keys, + function (a, b) + if type(a) ~= type(b) then + return type(a) < type(b) + end + if type(a) == 'number' then + return a < b + else + return tostring(a) < tostring(b) + end + end) + local i = 0 + return function() + i = i + 1 + if keys[i] ~= nil then + return keys[i], unordered_tt[keys[i]] + end + end +end + +function base_class.table(self, tab) + assert(type(tab) == 'table') + if self.done[tab] then + error('cyclic reference', 0) + end + self.done[tab] = true + + local items = {'{'} + local previdx = 0 + self:indent_inc() + for idx, val in ordered_iter(tab) do + local errors, valok, valexpr, valnote, idxok, idxexpr, idxnote + errors = {} + -- push current index onto key path stack to make it available to sub-printers + table.insert(self.tab_key_path, idx) + + valok, valexpr, valnote = pcall(self.val2expr, self, val) + if not valok then + table.insert(errors, string.format('value: %s', valexpr)) + end + + local addidx + if previdx and type(idx) == 'number' and idx - 1 == previdx then + -- monotonic sequence, do not print key + previdx = idx + addidx = false + else + -- end of monotonic sequence + -- from now on print keys as well + previdx = nil + addidx = true + end + + if addidx then + idxok, idxexpr, idxnote = pcall(self.val2expr, self, idx) + if not idxok or idxexpr == 'nil' then + table.insert(errors, string.format('key: not serializable', idxexpr)) + end + end + + local item = '' + if #errors == 0 then + -- finally serialize one [key=]?value expression + local indent = self:indent_head() + local note + if addidx then + note = self:format_note(idxnote, nil, self.key_val_sep) + item = string.format('%s%s[%s]%s=%s', + indent, note, + idxexpr, self.key_val_sep, self.key_val_sep) + indent = '' + end + note = self:format_note(valnote, nil, self.item_sep) + item = item .. string.format('%s%s%s,', indent, note, valexpr) + else + local errmsg = string.format('cannot print %s = %s (%s)', + self:string(tostring(idx)), + self:string(tostring(val)), + table.concat(errors, ', ')) + if self.on_unrepresentable == 'error' then + error(errmsg, 0) + else + errmsg = string.format('--[[ missing %s ]]', errmsg) + item = errmsg + end + end + table.insert(items, item) + table.remove(self.tab_key_path) -- pop current index from key path stack + end -- one key+value + self:indent_dec() + table.insert(items, self:indent_head() .. '}') + return table.concat(items, self.item_sep), string.format('%s follows', tab) +end + +-- machine readable variant, cannot represent all types and repeated references to a table +local serializer_class = { + indent_step = 0, + item_sep = ' ', + key_val_sep = ' ', + __inst_mt = {} +} +-- inheritance form base class (for :new()) +setmetatable(serializer_class, { __index = base_class }) +-- class instances with following metatable inherit all class members +serializer_class.__inst_mt.__index = serializer_class + +local function static_serializer(val, on_unrepresentable) + local inst = serializer_class:new(on_unrepresentable) + local expr, note = inst:val2expr(val) + return string.format('%s%s', inst:format_note(note, nil, inst.item_sep), expr) + end + +-- human friendly variant, not stable and not intended for machine consumption +local pprinter_class = { + indent_step = 4, + item_sep = '\n', + key_val_sep = ' ', + __inst_mt = {}, +} + +-- should be always empty because pretty-printer has fallback for all types +function pprinter_class.format_note() + return '' +end + +function pprinter_class._fallback(self, val) + if self.on_unrepresentable == 'error' then + base_class._fallback(self, val) + end + return tostring(val) +end + +function pprinter_class.char_is_printable(_, c) + -- ASCII (from space to ~) + tab or newline + -- and not ' or \ + return ((c >= 0x20 and c < 0x7f) + or c == 0x09 or c == 0x0A) + and c ~= 0x27 and c ~= 0x5C +end + +-- "function" is a Lua keyword so assignment below is workaround to create +-- function pprinter_class.function(self, f) +pprinter_class['function'] = function(self, f) +-- thanks to AnandA777 from StackOverflow! Function funcsign is adapted version of +-- https://stackoverflow.com/questions/51095022/inspect-function-signature-in-lua-5-1 + assert(type(f) == 'function', "bad argument #1 to 'funcsign' (function expected)") + local debuginfo = debug.getinfo(f) + local func_args = {} + local args_str + if debuginfo.what == 'C' then -- names N/A + args_str = '(?)' + goto add_name + end + + pcall(function() + local oldhook + local delay = 2 + local function hook() + delay = delay - 1 + if delay == 0 then -- call this only for the introspected function + -- stack depth 2 is the introspected function + for i = 1, debuginfo.nparams do + local k = debug.getlocal(2, i) + table.insert(func_args, k) + end + if debuginfo.isvararg then + table.insert(func_args, "...") + end + debug.sethook(oldhook) + error('aborting the call to introspected function') + end + end + oldhook = debug.sethook(hook, "c") -- invoke hook() on function call + f(unpack({})) -- huh? + end) + args_str = "(" .. table.concat(func_args, ", ") .. ")" + ::add_name:: + local name + if #self.tab_key_path > 0 then + name = string.format('function %s', self.tab_key_path[#self.tab_key_path]) + else + name = 'function ' + end + return string.format('%s%s: %s', name, args_str, string.sub(tostring(f), 11)) +end + +-- default tostring method is better suited for human-intended output +function pprinter_class.number(_, number) + return tostring(number) +end + +local function deserialize_lua(serial) + assert(type(serial) == 'string') + local deserial_func = loadstring('return ' .. serial) + if type(deserial_func) ~= 'function' then + panic('input is not a valid Lua expression') + end + return deserial_func() +end + +setmetatable(pprinter_class, { __index = base_class }) +pprinter_class.__inst_mt.__index = pprinter_class + +local function static_pprint(val, on_unrepresentable) + local inst = pprinter_class:new(on_unrepresentable) + local expr, note = inst:val2expr(val) + return string.format('%s%s', inst:format_note(note, nil, inst.item_sep), expr) +end + +local M = { + serialize_lua = static_serializer, + deserialize_lua = deserialize_lua, + pprint = static_pprint +} + +return M |