diff --git a/Library/Repr.lua b/Library/Repr.lua new file mode 100644 index 0000000..24e538d --- /dev/null +++ b/Library/Repr.lua @@ -0,0 +1,317 @@ +require('strict') +local libraryUtil = require("libraryUtil") +local checkType = libraryUtil.checkType +local checkTypeForNamedArg = libraryUtil.checkTypeForNamedArg + +local defaultOptions = { + pretty = false, + tabs = true, + semicolons = false, + spaces = 4, + sortKeys = true, + depth = 0, +} + +-- Define the reprRecursive variable here so that we can call the reprRecursive +-- function from renderSequence and renderKeyValueTable without getting +-- "Tried to read nil global reprRecursive" errors. +local reprRecursive + +local luaKeywords = { + ["and"] = true, + ["break"] = true, + ["do"] = true, + ["else"] = true, + ["elseif"] = true, + ["end"] = true, + ["false"] = true, + ["for"] = true, + ["function"] = true, + ["if"] = true, + ["in"] = true, + ["local"] = true, + ["nil"] = true, + ["not"] = true, + ["or"] = true, + ["repeat"] = true, + ["return"] = true, + ["then"] = true, + ["true"] = true, + ["until"] = true, + ["while"] = true, +} + +--[[ +-- Whether the given value is a valid Lua identifier (i.e. whether it can be +-- used as a variable name.) +--]] +local function isLuaIdentifier(str) + return type(str) == "string" + -- Must start with a-z, A-Z or underscore, and can only contain + -- a-z, A-Z, 0-9 and underscore + and str:find("^[%a_][%a%d_]*$") ~= nil + -- Cannot be a Lua keyword + and not luaKeywords[str] +end + +--[[ +-- Render a string representation. +--]] +local function renderString(s) + return (("%q"):format(s):gsub("\\\n", "\\n")) +end + +--[[ +-- Render a number representation. +--]] +local function renderNumber(n) + if n == math.huge then + return "math.huge" + elseif n == -math.huge then + return "-math.huge" + else + return tostring(n) + end +end + +--[[ +-- Whether a table has a __tostring metamethod. +--]] +local function hasTostringMetamethod(t) + return getmetatable(t) and type(getmetatable(t).__tostring) == "function" +end + +--[[ +-- Pretty print a sequence of string representations. +-- This can be made to represent different constructs depending on the values +-- of prefix, suffix, and separator. The amount of whitespace is controlled by +-- the depth and indent parameters. +--]] +local function prettyPrintItemsAtDepth(items, prefix, suffix, separator, indent, depth) + local whitespaceAtCurrentDepth = "\n" .. indent:rep(depth) + local whitespaceAtNextDepth = whitespaceAtCurrentDepth .. indent + local ret = {prefix, whitespaceAtNextDepth} + local first = items[1] + if first ~= nil then + table.insert(ret, first) + end + for i = 2, #items do + table.insert(ret, separator) + table.insert(ret, whitespaceAtNextDepth) + table.insert(ret, items[i]) + end + table.insert(ret, whitespaceAtCurrentDepth) + table.insert(ret, suffix) + return table.concat(ret) +end + +--[[ +-- Render a sequence of string representations. +-- This can be made to represent different constructs depending on the values of +-- prefix, suffix and separator. +--]] +local function renderItems(items, prefix, suffix, separator) + return prefix .. table.concat(items, separator .. " ") .. suffix +end + +--[[ +-- Render a regular table (a non-cyclic table with no __tostring metamethod). +-- This can be a sequence table, a key-value table, or a mix of the two. +--]] +local function renderNormalTable(t, context, depth) + local items = {} + + -- Render the items in the sequence part + local seen = {} + for i, value in ipairs(t) do + table.insert(items, reprRecursive(t[i], context, depth + 1)) + seen[i] = true + end + + -- Render the items in the key-value part + local keyOrder = {} + local keyValueStrings = {} + for k, v in pairs(t) do + if not seen[k] then + local kStr = isLuaIdentifier(k) and k or ("[" .. reprRecursive(k, context, depth + 1) .. "]") + local vStr = reprRecursive(v, context, depth + 1) + table.insert(keyOrder, kStr) + keyValueStrings[kStr] = vStr + end + end + if context.sortKeys then + table.sort(keyOrder) + end + for _, kStr in ipairs(keyOrder) do + table.insert(items, string.format("%s = %s", kStr, keyValueStrings[kStr])) + end + + -- Render the table structure + local prefix = "{" + local suffix = "}" + if context.pretty then + return prettyPrintItemsAtDepth( + items, + prefix, + suffix, + context.separator, + context.indent, + depth + ) + else + return renderItems(items, prefix, suffix, context.separator) + end +end + +--[[ +-- Render the given table. +-- As well as rendering regular tables, this function also renders cyclic tables +-- and tables with a __tostring metamethod. +--]] +local function renderTable(t, context, depth) + if hasTostringMetamethod(t) then + return tostring(t) + elseif context.shown[t] then + return "{CYCLIC}" + end + context.shown[t] = true + local result = renderNormalTable(t, context, depth) + context.shown[t] = false + return result +end + +--[[ +-- Recursively render a string representation of the given value. +--]] +function reprRecursive(value, context, depth) + if value == nil then + return "nil" + end + local valueType = type(value) + if valueType == "boolean" then + return tostring(value) + elseif valueType == "number" then + return renderNumber(value) + elseif valueType == "string" then + return renderString(value) + elseif valueType == "table" then + return renderTable(value, context, depth) + else + return "<" .. valueType .. ">" + end +end + +--[[ +-- Normalize a table of options passed by the user. +-- Any values not specified will be assigned default values. +--]] +local function normalizeOptions(options) + options = options or {} + local ret = {} + for option, defaultValue in pairs(defaultOptions) do + local value = options[option] + if value ~= nil then + if type(value) == type(defaultValue) then + ret[option] = value + else + error( + string.format( + 'Invalid type for option "%s" (expected %s, received %s)', + option, + type(defaultValue), + type(value) + ), + 3 + ) + end + else + ret[option] = defaultValue + end + end + return ret +end + +--[[ +-- Get the indent from the options table. +--]] +local function getIndent(options) + if options.tabs then + return "\t" + else + return string.rep(" ", options.spaces) + end +end + +--[[ +-- Render a string representation of the given value. +--]] +local function repr(value, options) + checkType("repr", 2, options, "table", true) + + options = normalizeOptions(options) + local context = {} + + context.pretty = options.pretty + if context.pretty then + context.indent = getIndent(options) + else + context.indent = "" + end + + if options.semicolons then + context.separator = ";" + else + context.separator = "," + end + + context.sortKeys = options.sortKeys + context.shown = {} + local depth = options.depth + + return reprRecursive(value, context, depth) +end + +--[[ +-- Render a string representation of the given function invocation. +--]] +local function invocationRepr(keywordArgs) + checkType("invocationRepr", 1, keywordArgs, "table") + checkTypeForNamedArg("invocationRepr", "funcName", keywordArgs.funcName, "string") + checkTypeForNamedArg("invocationRepr", "args", keywordArgs.args, "table", true) + checkTypeForNamedArg("invocationRepr", "options", keywordArgs.options, "table", true) + + local options = normalizeOptions(keywordArgs.options) + local depth = options.depth + + options.depth = depth + 1 + local items = {} + if keywordArgs.args then + for _, arg in ipairs(keywordArgs.args) do + table.insert(items, repr(arg, options)) + end + end + + local prefix = "(" + local suffix = ")" + local separator = "," + local renderedArgs + if options.pretty then + renderedArgs = prettyPrintItemsAtDepth( + items, + prefix, + suffix, + separator, + getIndent(options), + depth + ) + else + renderedArgs = renderItems(items, prefix, suffix, separator) + end + return keywordArgs.funcName .. renderedArgs +end + +return { + _isLuaIdentifier = isLuaIdentifier, + repr = repr, + invocationRepr = invocationRepr, +}