diff --git a/.gitignore b/.gitignore
index 3c52315..34fb7c2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,5 @@
/luarocks
/lua
/*.rock
+/*.sublime-workspace
+profile.json
diff --git a/examples/debugger.cast b/examples/debugger.cast
new file mode 100644
index 0000000..3a5ff8d
--- /dev/null
+++ b/examples/debugger.cast
@@ -0,0 +1,42 @@
+{"version": 2, "width": 80, "height": 20, "timestamp": 1723577770, "env": {"SHELL": "/bin/zsh", "TERM": "xterm-256color"}}
+[0.592429, "o", "\u001b]1337;RemoteHost=martin@Martins-MacBook-Air.local\u0007\u001b]1337;CurrentDir=/Users/martin/Projects/blog-projects/lua-series\u0007\u001b]1337;ShellIntegrationVersion=12;shell=zsh\u0007"]
+[0.598541, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"]
+[0.60956, "o", "\u001b]133;D;0\u0007"]
+[0.613294, "o", "\u001b]1337;RemoteHost=martin@Martins-MacBook-Air.local\u0007\u001b]1337;CurrentDir=/Users/martin/Projects/blog-projects/lua-series\u0007"]
+[0.615079, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b]133;A\u0007\u001b[34m~/P/b/lua-series \u001b]133;B\u0007\u001b[K"]
+[0.615125, "o", "\u001b[?1h\u001b="]
+[0.615265, "o", "\r\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b]133;A\u0007\u001b[34m~/P/b/lua-series \u001b[1m\u001b[34m\u001b[34m▶\u001b[39m\u001b[0m \u001b]133;B\u0007\u001b[K"]
+[0.615272, "o", "\u001b[?2004h"]
+[0.657115, "o", "\r\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b]133;A\u0007\u001b[34m~/P/b/lua-series \u001b[1m\u001b[34m\u001b[34m▶\u001b[39m\u001b[0m \u001b]133;B\u0007\u001b[K\u001b[52C\u001b[1m\u001b[32mpart-4\u001b[39m\u001b[0m \u001b[1m\u001b[37m◼\u001b[39m\u001b[0m\u001b[60D"]
+[1.418115, "o", "\u001b[32ml\u001b[39m"]
+[1.65959, "o", "\b\u001b[32ml\u001b[32mu\u001b[39m"]
+[1.733899, "o", "\b\b\u001b[32ml\u001b[32mu\u001b[32ma\u001b[39m"]
+[1.77025, "o", " "]
+[2.160705, "o", "\b\u001b[1m\u001b[37m\u001b[45m \u001b[0m\u001b[39m\u001b[49m\u001b[4msrc/errors/debugger-in-place.lua\u001b[24m"]
+[2.791272, "o", "\u001b[?1l\u001b>"]
+[2.791977, "o", "\u001b[?2004l\r\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b]133;A\u0007\u001b[34m~/P/b/lua-series \u001b[1m\u001b[34m\u001b[34m▶\u001b[39m\u001b[0m \u001b]133;B\u0007"]
+[2.792025, "o", "\u001b[32mlua\u001b[39m\u001b[1m\u001b[37m\u001b[45m \u001b[0m\u001b[39m\u001b[49m\u001b[4msrc/errors/debugger-in-place.lua\u001b[24m\u001b[K\u001b[16C\u001b[1m\u001b[32mpart-4\u001b[39m\u001b[0m \u001b[1m\u001b[37m◼\u001b[39m\u001b[0m\u001b[24D"]
+[2.796134, "o", "\r\r\n"]
+[2.797405, "o", "\u001b]133;C;\u0007"]
+[2.804949, "o", "\u001b[33mdebugger.lua: \u001b[0mLoaded for Lua 5.4\r\n\u001b[91mERROR: \u001b[0m\"Function 'greet' expects a string as argument.\"\r\n\u001b[33mbreak via \u001b[91mdbg.error()\u001b[92m => \u001b[0m\u001b[94msrc/errors/debugger-in-place.lua\u001b[0m:\u001b[33m7\u001b[0m in chunk at \u001b[94msrc/errors/debugger-in-place.lua\u001b[0m:\u001b[33m5\u001b[0m\r\n\u001b[91mdebugger.lua> \u001b[0m"]
+[3.981536, "o", "t"]
+[5.063186, "o", "\r\n"]
+[5.063357, "o", "Inspecting frame 0\r\n\u001b[90m 0\u001b[0m\u001b[92m => \u001b[0m\u001b[94msrc/errors/debugger-in-place.lua\u001b[0m:\u001b[33m7\u001b[0m in chunk at \u001b[94msrc/errors/debugger-in-place.lua\u001b[0m:\u001b[33m5\u001b[0m\r\n\u001b[90m 1\u001b[0m \u001b[94m[C]\u001b[0m:\u001b[33m-1\u001b[0m in global '\u001b[94mxpcall\u001b[0m'\r\n\u001b[90m 2\u001b[0m \u001b[94msrc/debugger.lua\u001b[0m:\u001b[33m525\u001b[0m in local '\u001b[94mpcall\u001b[0m'\r\n\u001b[90m 3\u001b[0m \u001b[94msrc/errors/debugger-in-place.lua\u001b[0m:\u001b[33m12\u001b[0m in chunk at \u001b[94msrc/errors/debugger-in-place.lua\u001b[0m:\u001b[33m0\u001b[0m\r\n\u001b[90m 4\u001b[0m \u001b[94m[C]\u001b[0m:\u001b[33m-1\u001b[0m in chunk at \u001b[94m[C]\u001b[0m:\u001b[33m-1\u001b[0m\r\n\u001b[91mdebugger.lua> \u001b[0m"]
+[7.973055, "o", "q"]
+[8.955808, "o", "\r\n"]
+[8.957133, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"]
+[8.987298, "o", "\u001b]133;D;0\u0007"]
+[8.991631, "o", "\u001b]1337;RemoteHost=martin@Martins-MacBook-Air.local\u0007\u001b]1337;CurrentDir=/Users/martin/Projects/blog-projects/lua-series\u0007"]
+[8.996042, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b]133;A\u0007\u001b[34m~/P/b/lua-series \u001b[1m\u001b[34m\u001b[34m▶\u001b[39m\u001b[0m \u001b]133;B\u0007\u001b[K\u001b[52C\u001b[1m\u001b[32mpart-4\u001b[39m\u001b[0m \u001b[1m\u001b[37m◼\u001b[39m\u001b[0m\u001b[60D"]
+[8.996169, "o", "\u001b[?1h\u001b="]
+[8.996426, "o", "\r\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b]133;A\u0007\u001b[34m~/P/b/lua-series \u001b[1m\u001b[34m\u001b[34m▶\u001b[39m\u001b[0m \u001b]133;B\u0007\u001b[K\u001b[52C\u001b[1m\u001b[32mpart-4\u001b[39m\u001b[0m \u001b[1m\u001b[37m◼\u001b[39m\u001b[0m\u001b[60D"]
+[8.996534, "o", "\u001b[?2004h"]
+[9.059222, "o", "\r\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b]133;A\u0007\u001b[34m~/P/b/lua-series \u001b[1m\u001b[34m\u001b[34m▶\u001b[39m\u001b[0m \u001b]133;B\u0007\u001b[K\u001b[52C\u001b[1m\u001b[32mpart-4\u001b[39m\u001b[0m \u001b[1m\u001b[37m◼\u001b[39m\u001b[0m\u001b[60D"]
+[9.776086, "o", "\u001b[1m\u001b[31me\u001b[0m\u001b[39m"]
+[9.987056, "o", "\b\u001b[0m\u001b[32me\u001b[32mx\u001b[39m"]
+[10.10285, "o", "\b\b\u001b[1m\u001b[31me\u001b[1m\u001b[31mx\u001b[1m\u001b[31mi\u001b[0m\u001b[39m"]
+[10.187393, "o", "\b\b\b\u001b[0m\u001b[32me\u001b[0m\u001b[32mx\u001b[0m\u001b[32mi\u001b[32mt\u001b[39m"]
+[10.293036, "o", "\u001b[?1l\u001b>"]
+[10.293331, "o", "\u001b[?2004l\r\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b]133;A\u0007\u001b[34m~/P/b/lua-series \u001b[1m\u001b[34m\u001b[34m▶\u001b[39m\u001b[0m \u001b]133;B\u0007\u001b[32mexit\u001b[39m\u001b[K\u001b[48C\u001b[1m\u001b[32mpart-4\u001b[39m\u001b[0m \u001b[1m\u001b[37m◼\u001b[39m\u001b[0m\u001b[56D"]
+[10.294894, "o", "\r\r\n"]
+[10.295708, "o", "\u001b]133;C;\u0007"]
diff --git a/examples/debugger.svg b/examples/debugger.svg
new file mode 100644
index 0000000..224b185
--- /dev/null
+++ b/examples/debugger.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/lua-series-1.1.0-1.rockspec b/lua-series-1.1.0-1.rockspec
index a0e7d39..11c4843 100644
--- a/lua-series-1.1.0-1.rockspec
+++ b/lua-series-1.1.0-1.rockspec
@@ -9,7 +9,7 @@ description = {
summary = "The companion repository to my Lua blog series.",
detailed = [[
This package is for educational purposes and contains only some test
- code for my Lua series of articles.
+ and example code for my Lua series.
]],
homepage = "https://martin-fieber.de/series/lua/",
license = "MIT",
diff --git a/lua-series.code-workspace b/lua-series.code-workspace
new file mode 100644
index 0000000..794e0d9
--- /dev/null
+++ b/lua-series.code-workspace
@@ -0,0 +1,7 @@
+{
+ "folders": [
+ {
+ "path": "."
+ }
+ ],
+}
\ No newline at end of file
diff --git a/lua.sublime-project b/lua.sublime-project
new file mode 100644
index 0000000..1c3553b
--- /dev/null
+++ b/lua.sublime-project
@@ -0,0 +1,24 @@
+{
+ "folders": [
+ {
+ "path": ".",
+ }
+ ],
+ "build_systems": [
+ {
+ "name": "Lua",
+ "working_dir": "$project_path",
+ "cmd": ["lua", "$file"],
+ "file_regex": "^(?:lua:)?[\t ](...*?):([0-9]*):?([0-9]*)",
+ "selector": "source.lua"
+ }
+ ],
+ "settings": {
+ "auto_complete": true,
+ "LSP": {
+ "lua": {
+ "enabled": true,
+ },
+ },
+ },
+}
diff --git a/src/debugger.lua b/src/debugger.lua
new file mode 100644
index 0000000..9de5f24
--- /dev/null
+++ b/src/debugger.lua
@@ -0,0 +1,655 @@
+-- SPDX-License-Identifier: MIT
+-- Copyright (c) 2024 Scott Lembcke and Howling Moon Software
+
+local dbg
+
+-- Use ANSI color codes in the prompt by default.
+local COLOR_GRAY = ""
+local COLOR_RED = ""
+local COLOR_BLUE = ""
+local COLOR_YELLOW = ""
+local COLOR_RESET = ""
+local GREEN_CARET = " => "
+
+local function pretty(obj, max_depth)
+ if max_depth == nil then max_depth = dbg.pretty_depth end
+
+ -- Returns true if a table has a __tostring metamethod.
+ local function coerceable(tbl)
+ local meta = getmetatable(tbl)
+ return (meta and meta.__tostring)
+ end
+
+ local function recurse(obj, depth)
+ if type(obj) == "string" then
+ -- Dump the string so that escape sequences are printed.
+ return string.format("%q", obj)
+ elseif type(obj) == "table" and depth < max_depth and not coerceable(obj) then
+ local str = "{"
+
+ for k, v in pairs(obj) do
+ local pair = pretty(k, 0).." = "..recurse(v, depth + 1)
+ str = str..(str == "{" and pair or ", "..pair)
+ end
+
+ return str.."}"
+ else
+ -- tostring() can fail if there is an error in a __tostring metamethod.
+ local success, value = pcall(function() return tostring(obj) end)
+ return (success and value or "")
+ end
+ end
+
+ return recurse(obj, 0)
+end
+
+-- The stack level that cmd_* functions use to access locals or info
+-- The structure of the code very carefully ensures this.
+local CMD_STACK_LEVEL = 6
+
+-- Location of the top of the stack outside of the debugger.
+-- Adjusted by some debugger entrypoints.
+local stack_top = 0
+
+-- The current stack frame index.
+-- Changed using the up/down commands
+local stack_inspect_offset = 0
+
+-- LuaJIT has an off by one bug when setting local variables.
+local LUA_JIT_SETLOCAL_WORKAROUND = 0
+
+-- Default dbg.read function
+local function dbg_read(prompt)
+ dbg.write(prompt)
+ io.flush()
+ return io.read()
+end
+
+-- Default dbg.write function
+local function dbg_write(str)
+ io.write(str)
+end
+
+local function dbg_writeln(str, ...)
+ if select("#", ...) == 0 then
+ dbg.write((str or "").."\n")
+ else
+ dbg.write(string.format(str.."\n", ...))
+ end
+end
+
+local function format_loc(file, line) return COLOR_BLUE..file..COLOR_RESET..":"..COLOR_YELLOW..line..COLOR_RESET end
+local function format_stack_frame_info(info)
+ local filename = info.source:match("@(.*)")
+ local source = filename and dbg.shorten_path(filename) or info.short_src
+ local namewhat = (info.namewhat == "" and "chunk at" or info.namewhat)
+ local name = (info.name and "'"..COLOR_BLUE..info.name..COLOR_RESET.."'" or format_loc(source, info.linedefined))
+ return format_loc(source, info.currentline).." in "..namewhat.." "..name
+end
+
+local repl
+
+-- Return false for stack frames without source,
+-- which includes C frames, Lua bytecode, and `loadstring` functions
+local function frame_has_line(info) return info.currentline >= 0 end
+
+local function hook_factory(repl_threshold)
+ return function(offset, reason)
+ return function(event, _)
+ -- Skip events that don't have line information.
+ if not frame_has_line(debug.getinfo(2)) then return end
+
+ -- Tail calls are specifically ignored since they also will have tail returns to balance out.
+ if event == "call" then
+ offset = offset + 1
+ elseif event == "return" and offset > repl_threshold then
+ offset = offset - 1
+ elseif event == "line" and offset <= repl_threshold then
+ repl(reason)
+ end
+ end
+ end
+end
+
+local hook_step = hook_factory(1)
+local hook_next = hook_factory(0)
+local hook_finish = hook_factory(-1)
+
+-- Create a table of all the locally accessible variables.
+-- Globals are not included when running the locals command, but are when running the print command.
+local function local_bindings(offset, include_globals)
+ local level = offset + stack_inspect_offset + CMD_STACK_LEVEL
+ local func = debug.getinfo(level).func
+ local bindings = {}
+
+ -- Retrieve the upvalues
+ do local i = 1; while true do
+ local name, value = debug.getupvalue(func, i)
+ if not name then break end
+ bindings[name] = value
+ i = i + 1
+ end end
+
+ -- Retrieve the locals (overwriting any upvalues)
+ do local i = 1; while true do
+ local name, value = debug.getlocal(level, i)
+ if not name then break end
+ bindings[name] = value
+ i = i + 1
+ end end
+
+ -- Retrieve the varargs (works in Lua 5.2 and LuaJIT)
+ local varargs = {}
+ do local i = 1; while true do
+ local name, value = debug.getlocal(level, -i)
+ if not name then break end
+ varargs[i] = value
+ i = i + 1
+ end end
+ if #varargs > 0 then bindings["..."] = varargs end
+
+ if include_globals then
+ -- In Lua 5.2, you have to get the environment table from the function's locals.
+ local env = (_VERSION <= "Lua 5.1" and getfenv(func) or bindings._ENV)
+ return setmetatable(bindings, {__index = env or _G})
+ else
+ return bindings
+ end
+end
+
+-- Used as a __newindex metamethod to modify variables in cmd_eval().
+local function mutate_bindings(_, name, value)
+ local FUNC_STACK_OFFSET = 3 -- Stack depth of this function.
+ local level = stack_inspect_offset + FUNC_STACK_OFFSET + CMD_STACK_LEVEL
+
+ -- Set a local.
+ do local i = 1; repeat
+ local var = debug.getlocal(level, i)
+ if name == var then
+ dbg_writeln(COLOR_YELLOW.."debugger.lua"..GREEN_CARET.."Set local variable "..COLOR_BLUE..name..COLOR_RESET)
+ return debug.setlocal(level + LUA_JIT_SETLOCAL_WORKAROUND, i, value)
+ end
+ i = i + 1
+ until var == nil end
+
+ -- Set an upvalue.
+ local func = debug.getinfo(level).func
+ do local i = 1; repeat
+ local var = debug.getupvalue(func, i)
+ if name == var then
+ dbg_writeln(COLOR_YELLOW.."debugger.lua"..GREEN_CARET.."Set upvalue "..COLOR_BLUE..name..COLOR_RESET)
+ return debug.setupvalue(func, i, value)
+ end
+ i = i + 1
+ until var == nil end
+
+ -- Set a global.
+ dbg_writeln(COLOR_YELLOW.."debugger.lua"..GREEN_CARET.."Set global variable "..COLOR_BLUE..name..COLOR_RESET)
+ _G[name] = value
+end
+
+-- Compile an expression with the given variable bindings.
+local function compile_chunk(block, env)
+ local source = "debugger.lua REPL"
+ local chunk = nil
+
+ if _VERSION <= "Lua 5.1" then
+ chunk = loadstring(block, source)
+ if chunk then setfenv(chunk, env) end
+ else
+ -- The Lua 5.2 way is a bit cleaner
+ chunk = load(block, source, "t", env)
+ end
+
+ if not chunk then dbg_writeln(COLOR_RED.."Error: Could not compile block:\n"..COLOR_RESET..block) end
+ return chunk
+end
+
+local SOURCE_CACHE = {}
+
+local function where(info, context_lines)
+ local source = SOURCE_CACHE[info.source]
+ if not source then
+ source = {}
+ local filename = info.source:match("@(.*)")
+ if filename then
+ pcall(function() for line in io.lines(filename) do table.insert(source, line) end end)
+ elseif info.source then
+ for line in info.source:gmatch("(.-)\n") do table.insert(source, line) end
+ end
+ SOURCE_CACHE[info.source] = source
+ end
+
+ if source and source[info.currentline] then
+ for i = info.currentline - context_lines, info.currentline + context_lines do
+ local tab_or_caret = (i == info.currentline and GREEN_CARET or " ")
+ local line = source[i]
+ if line then dbg_writeln(COLOR_GRAY.."% 4d"..tab_or_caret.."%s", i, line) end
+ end
+ else
+ dbg_writeln(COLOR_RED.."Error: Source not available for "..COLOR_BLUE..info.short_src);
+ end
+
+ return false
+end
+
+-- Wee version differences
+local unpack = unpack or table.unpack
+local pack = function(...) return {n = select("#", ...), ...} end
+
+local function cmd_step()
+ stack_inspect_offset = stack_top
+ return true, hook_step
+end
+
+local function cmd_next()
+ stack_inspect_offset = stack_top
+ return true, hook_next
+end
+
+local function cmd_finish()
+ local offset = stack_top - stack_inspect_offset
+ stack_inspect_offset = stack_top
+ return true, offset < 0 and hook_factory(offset - 1) or hook_finish
+end
+
+local function cmd_print(expr)
+ local env = local_bindings(1, true)
+ local chunk = compile_chunk("return "..expr, env)
+ if chunk == nil then return false end
+
+ -- Call the chunk and collect the results.
+ local results = pack(pcall(chunk, unpack(rawget(env, "...") or {})))
+
+ -- The first result is the pcall error.
+ if not results[1] then
+ dbg_writeln(COLOR_RED.."Error:"..COLOR_RESET.." "..results[2])
+ else
+ local output = ""
+ for i = 2, results.n do
+ output = output..(i ~= 2 and ", " or "")..dbg.pretty(results[i])
+ end
+
+ if output == "" then output = "" end
+ dbg_writeln(COLOR_BLUE..expr.. GREEN_CARET..output)
+ end
+
+ return false
+end
+
+local function cmd_eval(code)
+ local env = local_bindings(1, true)
+ local mutable_env = setmetatable({}, {
+ __index = env,
+ __newindex = mutate_bindings,
+ })
+
+ local chunk = compile_chunk(code, mutable_env)
+ if chunk == nil then return false end
+
+ -- Call the chunk and collect the results.
+ local success, err = pcall(chunk, unpack(rawget(env, "...") or {}))
+ if not success then
+ dbg_writeln(COLOR_RED.."Error:"..COLOR_RESET.." "..tostring(err))
+ end
+
+ return false
+end
+
+local function cmd_down()
+ local offset = stack_inspect_offset
+ local info
+
+ repeat -- Find the next frame with a file.
+ offset = offset + 1
+ info = debug.getinfo(offset + CMD_STACK_LEVEL)
+ until not info or frame_has_line(info)
+
+ if info then
+ stack_inspect_offset = offset
+ dbg_writeln("Inspecting frame: "..format_stack_frame_info(info))
+ if tonumber(dbg.auto_where) then where(info, dbg.auto_where) end
+ else
+ info = debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL)
+ dbg_writeln("Already at the bottom of the stack.")
+ end
+
+ return false
+end
+
+local function cmd_up()
+ local offset = stack_inspect_offset
+ local info
+
+ repeat -- Find the next frame with a file.
+ offset = offset - 1
+ if offset < stack_top then info = nil; break end
+ info = debug.getinfo(offset + CMD_STACK_LEVEL)
+ until frame_has_line(info)
+
+ if info then
+ stack_inspect_offset = offset
+ dbg_writeln("Inspecting frame: "..format_stack_frame_info(info))
+ if tonumber(dbg.auto_where) then where(info, dbg.auto_where) end
+ else
+ info = debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL)
+ dbg_writeln("Already at the top of the stack.")
+ end
+
+ return false
+end
+
+local function cmd_where(context_lines)
+ local info = debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL)
+ return (info and where(info, tonumber(context_lines) or 5))
+end
+
+local function cmd_trace()
+ dbg_writeln("Inspecting frame %d", stack_inspect_offset - stack_top)
+ local i = 0; while true do
+ local info = debug.getinfo(stack_top + CMD_STACK_LEVEL + i)
+ if not info then break end
+
+ local is_current_frame = (i + stack_top == stack_inspect_offset)
+ local tab_or_caret = (is_current_frame and GREEN_CARET or " ")
+ dbg_writeln(COLOR_GRAY.."% 4d"..COLOR_RESET..tab_or_caret.."%s", i, format_stack_frame_info(info))
+ i = i + 1
+ end
+
+ return false
+end
+
+local function cmd_locals()
+ local bindings = local_bindings(1, false)
+
+ -- Get all the variable binding names and sort them
+ local keys = {}
+ for k, _ in pairs(bindings) do table.insert(keys, k) end
+ table.sort(keys)
+
+ for _, k in ipairs(keys) do
+ local v = bindings[k]
+
+ -- Skip the debugger object itself, "(*internal)" values, and Lua 5.2's _ENV object.
+ if not rawequal(v, dbg) and k ~= "_ENV" and not k:match("%(.*%)") then
+ dbg_writeln(" "..COLOR_BLUE..k.. GREEN_CARET..dbg.pretty(v))
+ end
+ end
+
+ return false
+end
+
+local function cmd_help()
+ dbg.write(""
+ ..COLOR_BLUE.." "..GREEN_CARET.."re-run last command\n"
+ ..COLOR_BLUE.." c"..COLOR_YELLOW.."(ontinue)"..GREEN_CARET.."continue execution\n"
+ ..COLOR_BLUE.." s"..COLOR_YELLOW.."(tep)"..GREEN_CARET.."step forward by one line (into functions)\n"
+ ..COLOR_BLUE.." n"..COLOR_YELLOW.."(ext)"..GREEN_CARET.."step forward by one line (skipping over functions)\n"
+ ..COLOR_BLUE.." f"..COLOR_YELLOW.."(inish)"..GREEN_CARET.."step forward until exiting the current function\n"
+ ..COLOR_BLUE.." u"..COLOR_YELLOW.."(p)"..GREEN_CARET.."move up the stack by one frame\n"
+ ..COLOR_BLUE.." d"..COLOR_YELLOW.."(own)"..GREEN_CARET.."move down the stack by one frame\n"
+ ..COLOR_BLUE.." w"..COLOR_YELLOW.."(here) "..COLOR_BLUE.."[line count]"..GREEN_CARET.."print source code around the current line\n"
+ ..COLOR_BLUE.." e"..COLOR_YELLOW.."(val) "..COLOR_BLUE.."[statement]"..GREEN_CARET.."execute the statement\n"
+ ..COLOR_BLUE.." p"..COLOR_YELLOW.."(rint) "..COLOR_BLUE.."[expression]"..GREEN_CARET.."execute the expression and print the result\n"
+ ..COLOR_BLUE.." t"..COLOR_YELLOW.."(race)"..GREEN_CARET.."print the stack trace\n"
+ ..COLOR_BLUE.." l"..COLOR_YELLOW.."(ocals)"..GREEN_CARET.."print the function arguments, locals and upvalues.\n"
+ ..COLOR_BLUE.." h"..COLOR_YELLOW.."(elp)"..GREEN_CARET.."print this message\n"
+ ..COLOR_BLUE.." q"..COLOR_YELLOW.."(uit)"..GREEN_CARET.."halt execution\n"
+ )
+ return false
+end
+
+local last_cmd = false
+
+local commands = {
+ ["^c$"] = function() return true end,
+ ["^s$"] = cmd_step,
+ ["^n$"] = cmd_next,
+ ["^f$"] = cmd_finish,
+ ["^p%s+(.*)$"] = cmd_print,
+ ["^e%s+(.*)$"] = cmd_eval,
+ ["^u$"] = cmd_up,
+ ["^d$"] = cmd_down,
+ ["^w%s*(%d*)$"] = cmd_where,
+ ["^t$"] = cmd_trace,
+ ["^l$"] = cmd_locals,
+ ["^h$"] = cmd_help,
+ ["^q$"] = function() dbg.exit(0); return true end,
+}
+
+local function match_command(line)
+ for pat, func in pairs(commands) do
+ -- Return the matching command and capture argument.
+ if line:find(pat) then return func, line:match(pat) end
+ end
+end
+
+-- Run a command line
+-- Returns true if the REPL should exit and the hook function factory
+local function run_command(line)
+ -- GDB/LLDB exit on ctrl-d
+ if line == nil then dbg.exit(1); return true end
+
+ -- Re-execute the last command if you press return.
+ if line == "" then line = last_cmd or "h" end
+
+ local command, command_arg = match_command(line)
+ if command then
+ last_cmd = line
+ -- unpack({...}) prevents tail call elimination so the stack frame indices are predictable.
+ return unpack({command(command_arg)})
+ elseif dbg.auto_eval then
+ return unpack({cmd_eval(line)})
+ else
+ dbg_writeln(COLOR_RED.."Error:"..COLOR_RESET.." command '%s' not recognized.\nType 'h' and press return for a command list.", line)
+ return false
+ end
+end
+
+repl = function(reason)
+ -- Skip frames without source info.
+ while not frame_has_line(debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL - 3)) do
+ stack_inspect_offset = stack_inspect_offset + 1
+ end
+
+ local info = debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL - 3)
+ reason = reason and (COLOR_YELLOW.."break via "..COLOR_RED..reason..GREEN_CARET) or ""
+ dbg_writeln(reason..format_stack_frame_info(info))
+
+ if tonumber(dbg.auto_where) then where(info, dbg.auto_where) end
+
+ repeat
+ local success, done, hook = pcall(run_command, dbg.read(COLOR_RED.."debugger.lua> "..COLOR_RESET))
+ if success then
+ debug.sethook(hook and hook(0), "crl")
+ else
+ local message = COLOR_RED.."INTERNAL DEBUGGER.LUA ERROR. ABORTING\n:"..COLOR_RESET.." "..done
+ dbg_writeln(message)
+ error(message)
+ end
+ until done
+end
+
+-- Make the debugger object callable like a function.
+dbg = setmetatable({}, {
+ __call = function(_, condition, top_offset, source)
+ if condition then return end
+
+ top_offset = (top_offset or 0)
+ stack_inspect_offset = top_offset
+ stack_top = top_offset
+
+ debug.sethook(hook_next(1, source or "dbg()"), "crl")
+ return
+ end,
+})
+
+-- Expose the debugger's IO functions.
+dbg.read = dbg_read
+dbg.write = dbg_write
+dbg.shorten_path = function (path) return path end
+dbg.exit = function(err) os.exit(err) end
+
+dbg.writeln = dbg_writeln
+
+dbg.pretty_depth = 3
+dbg.pretty = pretty
+dbg.pp = function(value, depth) dbg_writeln(dbg.pretty(value, depth)) end
+
+dbg.auto_where = false
+dbg.auto_eval = false
+
+local lua_error, lua_assert = error, assert
+
+-- Works like error(), but invokes the debugger.
+function dbg.error(err, level)
+ level = level or 1
+ dbg_writeln(COLOR_RED.."ERROR: "..COLOR_RESET..dbg.pretty(err))
+ dbg(false, level, "dbg.error()")
+
+ lua_error(err, level)
+end
+
+-- Works like assert(), but invokes the debugger on a failure.
+function dbg.assert(condition, message)
+ if not condition then
+ dbg_writeln(COLOR_RED.."ERROR:"..COLOR_RESET..message)
+ dbg(false, 1, "dbg.assert()")
+ end
+
+ return lua_assert(condition, message)
+end
+
+-- Works like pcall(), but invokes the debugger on an error.
+function dbg.call(f, ...)
+ return xpcall(f, function(err)
+ dbg_writeln(COLOR_RED.."ERROR: "..COLOR_RESET..dbg.pretty(err))
+ dbg(false, 1, "dbg.call()")
+
+ return err
+ end, ...)
+end
+
+-- Error message handler that can be used with lua_pcall().
+function dbg.msgh(...)
+ if debug.getinfo(2) then
+ dbg_writeln(COLOR_RED.."ERROR: "..COLOR_RESET..dbg.pretty(...))
+ dbg(false, 1, "dbg.msgh()")
+ else
+ dbg_writeln(COLOR_RED.."debugger.lua: "..COLOR_RESET.."Error did not occur in Lua code. Execution will continue after dbg_pcall().")
+ end
+
+ return ...
+end
+
+-- Assume stdin/out are TTYs unless we can use LuaJIT's FFI to properly check them.
+local stdin_isatty = true
+local stdout_isatty = true
+
+-- Conditionally enable the LuaJIT FFI.
+local ffi = (jit and require("ffi"))
+if ffi then
+ ffi.cdef[[
+ int isatty(int); // Unix
+ int _isatty(int); // Windows
+ void free(void *ptr);
+
+ char *readline(const char *);
+ int add_history(const char *);
+ ]]
+
+ local function get_func_or_nil(sym)
+ local success, func = pcall(function() return ffi.C[sym] end)
+ return success and func or nil
+ end
+
+ local isatty = get_func_or_nil("isatty") or get_func_or_nil("_isatty") or (ffi.load("ucrtbase"))["_isatty"]
+ stdin_isatty = isatty(0)
+ stdout_isatty = isatty(1)
+end
+
+-- Conditionally enable color support.
+local color_maybe_supported = (stdout_isatty and os.getenv("TERM") and os.getenv("TERM") ~= "dumb")
+if color_maybe_supported and not os.getenv("DBG_NOCOLOR") then
+ COLOR_GRAY = string.char(27) .. "[90m"
+ COLOR_RED = string.char(27) .. "[91m"
+ COLOR_BLUE = string.char(27) .. "[94m"
+ COLOR_YELLOW = string.char(27) .. "[33m"
+ COLOR_RESET = string.char(27) .. "[0m"
+ GREEN_CARET = string.char(27) .. "[92m => "..COLOR_RESET
+end
+
+if stdin_isatty and not os.getenv("DBG_NOREADLINE") then
+ pcall(function()
+ local linenoise = require 'linenoise'
+
+ -- Load command history from ~/.lua_history
+ local hist_path = os.getenv('HOME') .. '/.lua_history'
+ linenoise.historyload(hist_path)
+ linenoise.historysetmaxlen(50)
+
+ local function autocomplete(env, input, matches)
+ for name, _ in pairs(env) do
+ if name:match('^' .. input .. '.*') then
+ linenoise.addcompletion(matches, name)
+ end
+ end
+ end
+
+ -- Auto-completion for locals and globals
+ linenoise.setcompletion(function(matches, input)
+ -- First, check the locals and upvalues.
+ local env = local_bindings(1, true)
+ autocomplete(env, input, matches)
+
+ -- Then, check the implicit environment.
+ env = getmetatable(env).__index
+ autocomplete(env, input, matches)
+ end)
+
+ dbg.read = function(prompt)
+ local str = linenoise.linenoise(prompt)
+ if str and not str:match "^%s*$" then
+ linenoise.historyadd(str)
+ linenoise.historysave(hist_path)
+ end
+ return str
+ end
+ dbg_writeln(COLOR_YELLOW.."debugger.lua: "..COLOR_RESET.."Linenoise support enabled.")
+ end)
+
+ -- Conditionally enable LuaJIT readline support.
+ pcall(function()
+ if dbg.read == nil and ffi then
+ local readline = ffi.load("readline")
+ dbg.read = function(prompt)
+ local cstr = readline.readline(prompt)
+ if cstr ~= nil then
+ local str = ffi.string(cstr)
+ if string.match(str, "[^%s]+") then
+ readline.add_history(cstr)
+ end
+
+ ffi.C.free(cstr)
+ return str
+ else
+ return nil
+ end
+ end
+ dbg_writeln(COLOR_YELLOW.."debugger.lua: "..COLOR_RESET.."Readline support enabled.")
+ end
+ end)
+end
+
+-- Detect Lua version.
+if jit then -- LuaJIT
+ LUA_JIT_SETLOCAL_WORKAROUND = -1
+ dbg_writeln(COLOR_YELLOW.."debugger.lua: "..COLOR_RESET.."Loaded for "..jit.version)
+elseif "Lua 5.1" <= _VERSION and _VERSION <= "Lua 5.4" then
+ dbg_writeln(COLOR_YELLOW.."debugger.lua: "..COLOR_RESET.."Loaded for ".._VERSION)
+else
+ dbg_writeln(COLOR_YELLOW.."debugger.lua: "..COLOR_RESET.."Not tested against ".._VERSION)
+ dbg_writeln("Please send me feedback!")
+end
+
+return dbg
diff --git a/src/errors/built-in-nested.lua b/src/errors/built-in-nested.lua
new file mode 100644
index 0000000..396622c
--- /dev/null
+++ b/src/errors/built-in-nested.lua
@@ -0,0 +1,10 @@
+local function will_error()
+ local non_table = 1
+ return non_table[1]
+end
+
+local function parent_function()
+ will_error()
+end
+
+parent_function()
diff --git a/src/errors/built-in.lua b/src/errors/built-in.lua
new file mode 100644
index 0000000..2e38d5b
--- /dev/null
+++ b/src/errors/built-in.lua
@@ -0,0 +1,2 @@
+local non_table = 1
+print(non_table[1])
diff --git a/src/errors/custom-error-pcall.lua b/src/errors/custom-error-pcall.lua
new file mode 100644
index 0000000..087f5d0
--- /dev/null
+++ b/src/errors/custom-error-pcall.lua
@@ -0,0 +1,15 @@
+local function work(str)
+ if type(str) ~= "string" then
+ error({
+ message = "Function 'work' needs a string.",
+ status = 42
+ }, 2)
+ end
+ return "Doing some work: " .. str
+end
+
+local status, result = pcall(work, 1)
+
+if not status then
+ print(result.message)
+end
diff --git a/src/errors/custom-error-xpcall.lua b/src/errors/custom-error-xpcall.lua
new file mode 100644
index 0000000..e5f8514
--- /dev/null
+++ b/src/errors/custom-error-xpcall.lua
@@ -0,0 +1,22 @@
+local function work(str)
+ if type(str) ~= "string" then
+ error({
+ message = "Function 'work' needs a string.",
+ status = 42
+ }, 2)
+ end
+ return "Doing some work: " .. str
+end
+
+local function handle_error(err)
+ if err.status == 42 then
+ -- Any special handling
+ end
+ return err.message .. "\n" .. debug.traceback()
+end
+
+local status, result = xpcall(work, handle_error, 1)
+
+if not status then
+ print(result)
+end
diff --git a/src/errors/custom-error.lua b/src/errors/custom-error.lua
new file mode 100644
index 0000000..6885460
--- /dev/null
+++ b/src/errors/custom-error.lua
@@ -0,0 +1,11 @@
+local function work(str)
+ if type(str) ~= "string" then
+ error({
+ message = "Function 'work' needs a string.",
+ status = 42
+ }, 2)
+ end
+ return "Doing some work: " .. str
+end
+
+work(1)
diff --git a/src/errors/debugger-in-place.lua b/src/errors/debugger-in-place.lua
new file mode 100644
index 0000000..a98422f
--- /dev/null
+++ b/src/errors/debugger-in-place.lua
@@ -0,0 +1,16 @@
+local dbg = require "debugger"
+local error = dbg.error
+local pcall = dbg.call
+
+local function greet(str)
+ if type(str) ~= "string" then
+ error("Function 'greet' expects a string as argument.")
+ end
+ return "Hello, " .. str
+end
+
+if pcall(greet, 1) then
+ print("No errors")
+else
+ print("Error calling greet!")
+end
diff --git a/src/errors/debugger.lua b/src/errors/debugger.lua
new file mode 100644
index 0000000..895aa60
--- /dev/null
+++ b/src/errors/debugger.lua
@@ -0,0 +1,32 @@
+local dbg = require "debugger"
+
+-- Breakpoint
+dbg()
+
+local function run(levels)
+ for _, level in ipairs(levels) do
+ local result = level()
+ -- Assert usage
+ dbg(result.success ~= true)
+ if result.success == false then
+ print("You lose!")
+ return
+ end
+ end
+
+ print("Winner!")
+end
+
+local function level1()
+ local message = "Level 1"
+ print(message)
+ return { success = true, points = 10 }
+end
+
+local function level2()
+ local message = "Level 2"
+ print(message)
+ return { success = false, points = 20 }
+end
+
+print(run({ level1, level2 }))
diff --git a/src/errors/pcall-return.lua b/src/errors/pcall-return.lua
new file mode 100644
index 0000000..45c5f57
--- /dev/null
+++ b/src/errors/pcall-return.lua
@@ -0,0 +1,14 @@
+local function greet(str)
+ if type(str) ~= "string" then
+ error("Function 'greet' expects a string as argument.", 2)
+ end
+ return "Hello, " .. str
+end
+
+local status, result = pcall(greet, 1)
+
+if status then
+ print(result)
+else
+ print("Error calling greet:\n", result)
+end
diff --git a/src/errors/pcall.lua b/src/errors/pcall.lua
new file mode 100644
index 0000000..63263b7
--- /dev/null
+++ b/src/errors/pcall.lua
@@ -0,0 +1,12 @@
+local function greet(str)
+ if type(str) ~= "string" then
+ error("Function 'greet' expects a string as argument.")
+ end
+ return "Hello, " .. str
+end
+
+if pcall(greet, 1) then
+ print("No errors")
+else
+ print("Error calling greet!")
+end
diff --git a/src/errors/print-debug.lua b/src/errors/print-debug.lua
new file mode 100644
index 0000000..8b485d4
--- /dev/null
+++ b/src/errors/print-debug.lua
@@ -0,0 +1,15 @@
+local function caller(fn)
+ -- Some code ...
+ local value = fn()
+ print("Problem at:",
+ debug.getinfo(fn).source
+ .. ":" ..
+ debug.getinfo(fn).linedefined)
+ return value
+end
+
+local function callback()
+ return "Great value!"
+end
+
+print(caller(callback))
diff --git a/src/errors/user-defined-origin.lua b/src/errors/user-defined-origin.lua
new file mode 100644
index 0000000..ca9de95
--- /dev/null
+++ b/src/errors/user-defined-origin.lua
@@ -0,0 +1,8 @@
+local function greet(str)
+ if type(str) ~= "string" then
+ error("Function 'greet' expects a string as argument.", 2)
+ end
+ return "Hello, " .. str
+end
+
+print(greet(1))
diff --git a/src/errors/user-defined.lua b/src/errors/user-defined.lua
new file mode 100644
index 0000000..1f85932
--- /dev/null
+++ b/src/errors/user-defined.lua
@@ -0,0 +1,8 @@
+local function greet(str)
+ if type(str) ~= "string" then
+ error("Function 'greet' expects a string as argument.")
+ end
+ return "Hello, " .. str
+end
+
+print(greet(1))
diff --git a/src/errors/xpcall-with-stacktrace.lua b/src/errors/xpcall-with-stacktrace.lua
new file mode 100644
index 0000000..68b08d8
--- /dev/null
+++ b/src/errors/xpcall-with-stacktrace.lua
@@ -0,0 +1,17 @@
+local function greet(str)
+ if type(str) ~= "string" then
+ error("Function 'greet' expects a string as argument.", 2)
+ end
+ return "Hello, " .. str
+end
+
+local function handle_error(err)
+ -- Call inside error handler:
+ return err .. "\n" .. debug.traceback()
+end
+
+local status, result = xpcall(greet, handle_error, 1)
+
+if not status then
+ print(result)
+end
diff --git a/src/errors/xpcall.lua b/src/errors/xpcall.lua
new file mode 100644
index 0000000..16a0373
--- /dev/null
+++ b/src/errors/xpcall.lua
@@ -0,0 +1,19 @@
+local function greet(str)
+ if type(str) ~= "string" then
+ error("Function 'greet' expects a string as argument.", 2)
+ end
+ return "Hello, " .. str
+end
+
+local function handle_error(err)
+ if err == "" then
+ return "Unknown error!"
+ end
+ return err
+end
+
+local status, result = xpcall(greet, handle_error, 1)
+
+if not status then
+ print("Error calling greet:\n", result)
+end
diff --git a/src/examples/assert-load.lua b/src/examples/assert-load.lua
new file mode 100644
index 0000000..60c1f0b
--- /dev/null
+++ b/src/examples/assert-load.lua
@@ -0,0 +1,8 @@
+local function readConfig(path)
+ local config = {}
+ assert(loadfile(path, "t", config))()
+ return config
+end
+
+local config = readConfig("src/examples/config.lua")
+print(config.speed, config.jumpHeight)
diff --git a/src/examples/assert.lua b/src/examples/assert.lua
index 5c57128..87073a6 100644
--- a/src/examples/assert.lua
+++ b/src/examples/assert.lua
@@ -1,19 +1,19 @@
-- Basic test wrapper
local function test(fn)
- local status, err = pcall(fn)
+ local status, result = pcall(fn)
if not status then
- print(err)
+ print(result)
end
end
-- Testing some code
-local factorial = require("examples/factorial")
+local factorial = require "examples/factorial"
-test(function ()
- assert( factorial(1) == 1, "Factor of 1 should be 1")
+test(function()
+ assert(factorial(1) == 1, "Factor of 1 should be 1")
end)
-test(function ()
+test(function()
assert(factorial(4) == 24, "Factor of 4 should be 24")
end)
diff --git a/src/examples/factorial-test-2.lua b/src/examples/factorial-test-2.lua
index c8441dd..e518008 100644
--- a/src/examples/factorial-test-2.lua
+++ b/src/examples/factorial-test-2.lua
@@ -1,8 +1,8 @@
-local tr = require("examples/test-runner")
-local factorial = require("examples/factorial")
+local tr = require "examples/test-runner"
+local factorial = require "examples/factorial"
tr:test(function ()
- assert( factorial(1) == 1, "Factor of 1 should be 1")
+ assert(factorial(1) == 1, "Factor of 1 should be 1")
end)
tr:test(function ()
diff --git a/src/examples/factorial-test.lua b/src/examples/factorial-test.lua
index df22fd0..b8825fe 100644
--- a/src/examples/factorial-test.lua
+++ b/src/examples/factorial-test.lua
@@ -1,17 +1,17 @@
-local lu = require("luaunit")
-local factorial = require("examples/factorial")
+local lu = require "luaunit"
+local factorial = require "examples/factorial"
local TestFactorial = {}
-function TestFactorial:test1()
+function TestFactorial.test1()
lu.assertEquals(factorial(0), 1)
end
-function TestFactorial:test2()
+function TestFactorial.test2()
lu.assertEquals(factorial(1), 1)
end
-function TestFactorial:test3()
+function TestFactorial.test3()
lu.assertEquals(factorial(4), 24)
end
diff --git a/src/examples/logger-test.lua b/src/examples/logger-test.lua
index dc71b3d..e3869f7 100644
--- a/src/examples/logger-test.lua
+++ b/src/examples/logger-test.lua
@@ -1,5 +1,5 @@
-local lu = require("luaunit")
-local Logger = require("examples/logger")
+require "luaunit"
+local Logger = require "examples/logger"
local TestLogger = {}
diff --git a/src/examples/logger.lua b/src/examples/logger.lua
index 3986d47..121c890 100644
--- a/src/examples/logger.lua
+++ b/src/examples/logger.lua
@@ -1,8 +1,8 @@
-- Example code for setUp and tearDown usage.
local Logger = {}
-function Logger:setup() end
+function Logger.setup() end
-function Logger:log() end
+function Logger.log() end
return Logger
diff --git a/src/examples/memory-profile.lua b/src/examples/memory-profile.lua
new file mode 100644
index 0000000..86aa371
--- /dev/null
+++ b/src/examples/memory-profile.lua
@@ -0,0 +1,18 @@
+print("Memory Before:\t", collectgarbage("count") .. " kb")
+
+local lu = require "luaunit"
+local round = require "examples/round"
+
+local TestRound = {}
+
+function TestRound.test1()
+ lu.assertEquals(round(3.44), 3)
+end
+
+function TestRound.test2()
+ lu.assertEquals(round(3.44, 1), 3.4)
+end
+
+print("Memory After:\t", collectgarbage("count") .. " kb")
+
+return TestRound
diff --git a/src/examples/mock-exported-test.lua b/src/examples/mock-exported-test.lua
index fd6f948..fd148c0 100644
--- a/src/examples/mock-exported-test.lua
+++ b/src/examples/mock-exported-test.lua
@@ -1,7 +1,7 @@
-local lu = require("luaunit")
+local lu = require "luaunit"
-- Representing a module to require. Example:
--- local someModule = require("some-module")
+-- local someModule = require "some-module"
local someModule = { fn = function () return false end }
local TestSomeModule = {}
@@ -14,7 +14,7 @@ function TestSomeModule:setUp()
someModule.fn = function () return true end
end
-function TestSomeModule:test1()
+function TestSomeModule.test1()
lu.assertTrue(someModule.fn())
end
diff --git a/src/examples/mock-global-test.lua b/src/examples/mock-global-test.lua
index 63b5bd6..8d65025 100644
--- a/src/examples/mock-global-test.lua
+++ b/src/examples/mock-global-test.lua
@@ -1,4 +1,4 @@
-local lu = require("luaunit")
+local lu = require "luaunit"
local function log(text) print(text) end
@@ -8,7 +8,7 @@ function TestGlobal:setUp()
self.backupPrint = _G.print
end
-function TestGlobal:test1()
+function TestGlobal.test1()
local hasBeenCalled = false
_G.print = function () hasBeenCalled = true end
log("Hello World")
diff --git a/src/examples/mock-module/assets-test.lua b/src/examples/mock-module/assets-test.lua
index c79d1c8..486d423 100644
--- a/src/examples/mock-module/assets-test.lua
+++ b/src/examples/mock-module/assets-test.lua
@@ -1,4 +1,4 @@
-local lu = require("luaunit")
+local lu = require "luaunit"
local TestAssets = {}
@@ -17,8 +17,8 @@ function TestAssets:setUp()
end
end
-function TestAssets:test1()
- local assets = require("examples/mock-module/assets")
+function TestAssets.test1()
+ local assets = require "examples/mock-module/assets"
lu.assertEquals(assets.getPath(), "/assets")
end
diff --git a/src/examples/mock-module/assets.lua b/src/examples/mock-module/assets.lua
index 6128f73..6412c84 100644
--- a/src/examples/mock-module/assets.lua
+++ b/src/examples/mock-module/assets.lua
@@ -1,8 +1,8 @@
-local path = require("path")
+local path = require "path"
local Assets = {}
-function Assets:getPath()
+function Assets.getPath()
local userHome = path.user_home()
return userHome .. path.DIR_SEP .. "assets"
end
diff --git a/src/examples/profiler-example.lua b/src/examples/profiler-example.lua
new file mode 100644
index 0000000..00e40f1
--- /dev/null
+++ b/src/examples/profiler-example.lua
@@ -0,0 +1,25 @@
+local profiler = require "tools/profiler-mhf"
+
+profiler:begin_session()
+
+local function test_fn()
+ local scope = profiler:scope("test scope")
+ print(42)
+ scope:close()
+end
+
+local function factorial(n)
+ if (n == 0) then
+ return 1
+ else
+ local _ = profiler:scope("test scope")
+ return n * factorial(n - 1)
+ end
+end
+
+factorial(2)
+test_fn()
+
+profiler:end_session()
+
+return factorial
diff --git a/src/examples/round-test.lua b/src/examples/round-test.lua
index 32ade6a..61110a1 100644
--- a/src/examples/round-test.lua
+++ b/src/examples/round-test.lua
@@ -1,13 +1,13 @@
-local lu = require("luaunit")
-local round = require("examples/round")
+local lu = require "luaunit"
+local round = require "examples/round"
local TestRound = {}
-function TestRound:test1()
+function TestRound.test1()
lu.assertEquals(round(3.44), 3)
end
-function TestRound:test2()
+function TestRound.test2()
lu.assertEquals(round(3.44, 1), 3.4)
end
diff --git a/src/examples/test-runner.lua b/src/examples/test-runner.lua
index f96d76b..8ef284d 100644
--- a/src/examples/test-runner.lua
+++ b/src/examples/test-runner.lua
@@ -3,10 +3,10 @@ local Runner = {
}
function Runner:test(fn)
- local status, err = pcall(fn)
+ local status, result = pcall(fn)
if not status then
self.hasErrors = true
- print(err)
+ print(result)
end
end
diff --git a/src/main.lua b/src/main.lua
index a09805d..26d8cf8 100644
--- a/src/main.lua
+++ b/src/main.lua
@@ -1,7 +1,9 @@
-local inspect = require("inspect")
+require "src/setup"
+
+local inspect = require "inspect"
local output = {
- "Hello, reader.",
- 42
+ "Hello, reader.",
+ 42
}
print(inspect(output))
diff --git a/src/setup.lua b/src/setup.lua
index 9b5ff6d..437303f 100644
--- a/src/setup.lua
+++ b/src/setup.lua
@@ -1,6 +1,6 @@
local version = _VERSION:match("%d+%.%d+")
package.path = "lua_modules/share/lua/" .. version ..
- "/?.lua;lua_modules/share/lua/" .. version .. "/?/init.lua;" ..
- package.path .. ";src/?.lua"
+ "/?.lua;lua_modules/share/lua/" .. version .. "/?/init.lua;" ..
+ package.path .. ";src/?.lua"
package.cpath = "lua_modules/lib/lua/" .. version .. "/?.so;" .. package.cpath
diff --git a/src/test.lua b/src/test.lua
index 3860a07..84069b9 100644
--- a/src/test.lua
+++ b/src/test.lua
@@ -1,10 +1,18 @@
-TestFactorial = require("examples/factorial-test")
-TestRound = require("examples/round-test")
-TestLogger = require("examples/logger-test")
-TestAssets = require("examples/mock-module/assets-test")
+local profiler = require "tools/profiler-mhf"
-TestMockExported = require("examples/mock-exported-test")
-TestMockGlobal = require("examples/mock-global-test")
+profiler:begin_session()
-local lu = require("luaunit")
-os.exit(lu.LuaUnit.run())
+TestFactorial = require "examples/factorial-test"
+TestRound = require "examples/round-test"
+TestLogger = require "examples/logger-test"
+TestAssets = require "examples/mock-module/assets-test"
+
+TestMockExported = require "examples/mock-exported-test"
+TestMockGlobal = require "examples/mock-global-test"
+
+local lu = require "luaunit"
+local status = lu.LuaUnit.run()
+
+profiler:end_session()
+
+os.exit(status)
diff --git a/src/tools/profiler-mhf.lua b/src/tools/profiler-mhf.lua
new file mode 100644
index 0000000..d798cc0
--- /dev/null
+++ b/src/tools/profiler-mhf.lua
@@ -0,0 +1,184 @@
+local debug = require "debug"
+
+local internal_functions = {}
+
+local Profile = {
+ file = "profile.json",
+ output = "",
+}
+
+function Profile:new(file)
+ local obj = {
+ file = file or self.file
+ }
+ self.__index = self
+ setmetatable(obj, self)
+ return obj
+end
+
+function Profile:write_header()
+ self.output = string.format([[{"metadata": {
+ "source": "CustomProfiler",
+ "startTime": "%s",
+ "dataOrigin": "TraceEvents"
+ },"traceEvents":[]], os.date("%Y-%m-%dT%X"))
+end
+
+function Profile:write_footer()
+ local file_handle = io.open(self.file, "w")
+ if file_handle then
+ file_handle:write(self.output:sub(1, -2) .. "]}")
+ self.output = ""
+ io.close(file_handle)
+ end
+end
+
+function Profile:write_item(name, start_time, end_time)
+ local json = [[
+ {
+ "args":{},
+ "cat": "function",
+ "dur": %f,
+ "name": "%s",
+ "ph": "X",
+ "pid": 0,
+ "tid": 0,
+ "ts": %f
+ },]]
+ self.output = self.output .. string.format(json, end_time, name, start_time)
+end
+
+local Instrumentator = {
+ -- Clock milliseconds to nanoseconds
+ clock = function() return os.clock() * 1000000 end,
+ profile = nil,
+ tail_calls = 0,
+ function_stack = {},
+ scope_stack = {},
+}
+
+function Instrumentator:create_hook()
+ return function(event, _line, info)
+ info = info or debug.getinfo(2)
+ local func = info.func
+
+ -- Ignore internal or C functions in trace
+ if internal_functions[func] or info.what ~= "Lua" then
+ return
+ end
+
+ local _, stack_depth = debug.traceback():gsub("\n", "\n")
+ if info.istailcall == true then
+ stack_depth = stack_depth - 1
+ end
+
+ if event == "tail call" then
+ self.tail_calls = self.tail_calls + 1
+
+ if not self.function_stack[func] then
+ self.function_stack[func] = {}
+ end
+
+ self.function_stack[func][stack_depth + self.tail_calls] = self.clock()
+ end
+
+ if event == "call" then
+ if not self.function_stack[func] then
+ self.function_stack[func] = {}
+ end
+
+ self.function_stack[func][stack_depth] = self.clock()
+ end
+
+ if event == "return" then
+ if not self.function_stack[func] then
+ return
+ end
+
+ local function _write(id, depth)
+ if self.function_stack[func][depth] == nil then
+ -- TODO: There is probably an issue with clculating the tail calls
+ -- depth here. Need to investigate further.
+ return
+ end
+
+ local name = info.name or string.format("%s(%s:%s)", id, info.short_src, info.linedefined)
+ local start_time = self.function_stack[func][depth]
+ local end_time = self.clock() - start_time
+ self.profile:write_item(name, start_time, end_time)
+ self.function_stack[func][depth] = nil
+ end
+
+ if info.istailcall == true then
+ for i = self.tail_calls, 1, -1 do
+ _write("tail_call", stack_depth + i)
+ end
+ self.tail_calls = 0
+ end
+
+ _write("unknown", stack_depth)
+
+ -- Cleanup
+ if next(self.function_stack[func]) == nil then
+ self.function_stack[func] = nil
+ end
+ end
+ end
+end
+
+function Instrumentator:begin_session(file)
+ if self.profile then
+ error(string.format(
+ "Instrumentor:begin_session('%s'), but another session '%s' is already open.",
+ file or "", self.profile.file), 2)
+ end
+
+ self.profile = Profile:new(file)
+ self.profile:write_header()
+
+ debug.sethook(self:create_hook(), "cr")
+end
+
+function Instrumentator:scope(name)
+ self.scope_stack[name] = self.clock()
+
+ local function __close()
+ if not self.scope_stack[name] then
+ return
+ end
+
+ local info = debug.getinfo(2)
+ local scope_name = string.format("%s(%s:%s)", name, info.short_src, info.linedefined)
+ local start_time = self.scope_stack[name]
+ local end_time = self.clock() - start_time
+ self.profile:write_item(scope_name, start_time, end_time)
+
+ self.scope_stack[name] = nil
+ end
+
+ local closable = { close = __close }
+ setmetatable(closable, { __close = __close })
+ internal_functions[__close] = true
+
+ return closable
+end
+
+function Instrumentator:end_session()
+ debug.sethook()
+ self.profile:write_footer()
+ self.profile = nil;
+end
+
+local function collect_function(from, into)
+ for _, v in pairs(from) do
+ if type(v) == "function" then
+ into[v] = true
+ end
+ end
+end
+
+internal_functions[collect_function] = true
+collect_function(Instrumentator, internal_functions)
+collect_function(Profile, internal_functions)
+
+return Instrumentator
diff --git a/src/tools/profiler-pil.lua b/src/tools/profiler-pil.lua
new file mode 100644
index 0000000..b292eec
--- /dev/null
+++ b/src/tools/profiler-pil.lua
@@ -0,0 +1,36 @@
+-- Example from "Programming in Lua"
+-- https://www.lua.org/pil/23.3.html
+local Counters = {}
+local Names = {}
+
+local function hook()
+ local f = debug.getinfo(2, "f").func
+ if Counters[f] == nil then -- first time `f' is called?
+ Counters[f] = 1
+ Names[f] = debug.getinfo(2, "Sn")
+ else -- only increment the counter
+ Counters[f] = Counters[f] + 1
+ end
+end
+
+local function getname(func)
+ local n = Names[func]
+ if n.what == "C" then
+ return n.name
+ end
+ local loc = string.format("[%s]:%s", n.short_src, n.linedefined)
+ if n.namewhat ~= "" then
+ return string.format("%s (%s)", loc, n.name)
+ else
+ return string.format("%s", loc)
+ end
+end
+
+local f = assert(loadfile(arg[1]))
+debug.sethook(hook, "c") -- turn on the hook
+f() -- run the main program
+debug.sethook() -- turn off the hook
+
+for func, count in pairs(Counters) do
+ print(getname(func), count)
+end