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 @@ +~/P/b/lua-series~/P/b/lua-series~/P/b/lua-seriespart-4~/P/b/lua-seriesluapart-4~/P/b/lua-seriesluasrc/errors/debugger-in-place.luapart-4debugger.lua:LoadedforLua5.4ERROR:"Function'greet'expectsastringasargument."breakviadbg.error()=>src/errors/debugger-in-place.lua:7inchunkatsrc/errors/debugger-in-place.lua:5debugger.lua>debugger.lua>tInspectingframe00=>src/errors/debugger-in-place.lua:7inchunkatsrc/errors/debugger-in-place.lua:51[C]:-1inglobal'xpcall'2src/debugger.lua:525inlocal'pcall'3src/errors/debugger-in-place.lua:12inchunkatsrc/errors/debugger-in-place.lua:04[C]:-1inchunkat[C]:-1debugger.lua>q~/P/b/lua-serieslpart-4~/P/b/lua-serieslupart-4 \ 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