Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
317 changes: 317 additions & 0 deletions Library/Repr.lua
Original file line number Diff line number Diff line change
@@ -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,
}