Warning: this is an htmlized version!
The original is here, and the conversion rules are here. |
-- ldb-edrx.lua - A Lua debugger -- This is Rici Lake's "ldb.lua", with some trivial changes by Edrx. -- Details later... -- $Id: ldb.lua,v 1.17 2007/04/18 21:24:24 jbloggs Exp $ -- -- Requires Lua 5.1 -- -- Written in a fit of boredom by Rici Lake. The author hereby -- releases the contents of this file into the public domain, -- disclaiming all rights and responsibilities. Do with it -- as you will. Caution: do not operate while under the influence -- of heavy machinery. -- local VERSION = ([[$Revision: 1.17 $]]):match":([^$]*)" local VERSION_DATE = ([[$Date: 2007/04/18 21:24:24 $]]):match":([^$]*)" local function Memoize(func) return setmetatable({}, {__index = function(t, k) local v = func(k); t[k] = v; return v end}) end local function words(s) local rv = {} for w in s:gmatch"%S+" do rv[w] = w end return rv end -- PLUGIN datastructures ---------------------------------------------------- -- -- SPLAIN -- the help/command parser -- command[name] -> function(state, args) -- The command functions -- By convention, the functions are written in O-O style, -- so that `self` is the state object. local command = {} -- splain[name] -> string -- If the key is present in command[], then this is help for a -- command. The first line of the string *must* have the format: -- "args - summary". The `- ` is mandatory, but `args` and the -- space which follows are optional. If an argument is optional, -- it should be surrounded by []. By convention, argument names -- are written in CAPITAL LETTERS. -- -- If the key is not present in command[], then this is a topic, -- and the first line of the string *must* have the format: -- "X summary" where `X` is either a `*` or a `-`. If it is -- a `*` then this is an "important" topic and the help system -- will print it first in the topics summary. -- -- If there is a long description, it should be separated from -- the summary line by a blank line, and be indented two spaces. -- Please keep lines to 78 characters including the indent. local splain = {} -- alias[string] -> string -- Commands can have at most one alias, and aliases should either -- be a single non-alphabetic character or a short alphabetic -- string (one or two characters). Topics cannot have aliases. -- If a non-alphabetic alias is more than one character long, -- it will not be recognized unless followed by a space. local alias = {} -- HANDLER hooks -- prehandler[string] -> function(arg2) -- handler[string] -> function(state, arg1, arg2) -- ldb() is always entered with two arguments, both of which are -- optional. When entered as an error function, the first argument -- will be the error object (usually a simple string message) and -- the second argument will usually be nil, but may be a hint from -- error() about which callframe is the user-visible error. When -- entered as a hook function, the first argument will be one of -- Lua's hook strings (line, call, count, return, 'tail return'). -- When called directly, the first argument could be anything, but -- all strings starting ldb_ are reserved. The only handler defined -- by the core system is ldb_cmd, which is used by the C glue. -- -- The prehandler is primarily used by the breakpoint system, in order -- to continue execution as fast as possible. It is called immediately -- when ldb is entered, and should either return nothing/nil/false as -- fast as possible, or return a new arg1, arg2 pair. In the first case, -- ldb is not entered at all; this is used to filter unwanted hook calls. -- It's probably not of much use otherwise. -- -- The handler is called once the debugger state has been set up, but -- before anything else has happened. It should either return: -- nil or nothing --> ldb will enter interactive mode -- true, value --> ldb will return the specified value. -- Note the different prototypes. prehandler was deliberately stripped -- to the minimum possible calling interface. -- local prehandler, handler = {}, {} -- STARTUP ------------------------------------------------------------------ local ldb -- Grab debug and other globals now, in case they get modified local _G = getfenv() local debug = debug local getfenv, getinfo = debug.getfenv, debug.getinfo local getupvalue, setupvalue = debug.getupvalue, debug.setupvalue local getlocal, setlocal = debug.getlocal, debug.setlocal local getmetatable, setmetatable = getmetatable, setmetatable local pcall = pcall local concat = table.concat local function prefer(pkgname) local ok, pkg = pcall(require, pkgname) return ok and pkg end -- valid keys in a stackframe (debug.getinfo + a few of our own) local infokeys = words[[ source short_src linedefined lastlinedefined what currentline name namewhat nups func var level ]] local infofuncs = {} function infofuncs.fenv(info) if info.func then return getfenv(info.func) else return {} end end -- Create an id line from an info table. This is similar -- but not identical to debug.traceback, but it can be customized -- TODO: Move this into the config system. local id do local function lineno(l, pfx) if l and l > 0 then return pfx..tostring(l) else return "" end end -- The default id doesn't use self function id(info) if info.what == "tail" then return "[tailcall]" end local segone if info.what == "main" then if info.source:sub(1,1) == "@" then segone = "file" elseif info.source:sub(1,1) == "=" then segone = info.source:sub(2) else segone = "??" end elseif info.namewhat and info.namewhat ~= "" then segone = info.namewhat else segone = "function" end local segs = { ("%-8s"):format(segone) } segs[#segs+1] = info.name local source = info.source and info.source:sub(1,1) == "@" and info.source:sub(2) or info.short_src segs[#segs+1] = source and ("<%s%s>"):format(source, lineno(info.linedefined, ":")) segs[#segs+1] = lineno(info.currentline, "at line ") return concat(segs, " ") end infofuncs.id = id end -- CONFIGURATION ------------------------------------------------------------ -- This is the pseudo-environment table which will be used -- by all instances of the debugger. It's not actually -- the debugger's environment table; rather, it's bound -- into the metamethods for debugger contexts, but it's -- pretty similar. A future version may allow multiple -- debuggers with different environments. local env = prefer"ldb-config" or {} if type(env) ~= "table" then print"ldb-config.lua must return a table of configuration settings" print"Ignoring ldb-config and using defaults" env = {} end -- Make it look like the config was loaded even if it wasn't. package.loaded["ldb-config"] = env -- Add the configuration hooks env.splain = splain env.alias = alias env.command = command env.prehandler = prehandler env.handler = handler -- Set up the default configuration env.PROMPT = env.PROMPT or "(ldb) " env.PROMPT2 = env.PROMPT2 or " >>> " env.input = env.input or function(prompt) io.write(prompt); return io.read"*l" end env.output = env.output or print env.error = env.error or env.output do local function donothing() end local function selfidentity(self, ...) return ... end env.enterframe = env.enterframe or selfidentity env.leave = env.leave or selfidentity env.edit = env.edit or donothing env.load_plugins = env.load_plugins or donothing end env.g = _G env.id = env.id or id setmetatable(env, {__index = _G}) -- This function finds the frame which invoked the debugger. -- It starts by finding the sentinel frame (placed on the -- callstack by the debugger) and then works backwards, -- skipping over the debugger frame itself and also -- error or assert, if present. It also tries to identify -- and skip the trampoline pushed onto the stack by lua.c. -- That would be easier if lua.c used a constant closure which -- we knew about. -- --[[ local function getoffset(sentinel) for base = 3, 1e6 do if getinfo(base, "f").func == sentinel then base = base + 2 local _, val = getlocal(base, 2) if val == debug and not getlocal(base, 3) then base = base + 1 end local errfunc = getinfo(base, "f").func if errfunc == error or errfunc == assert then base = base + 1 end return base - 2 end end end ]] local function getoffset(sentinel) for base = 3, 1e6 do if getinfo(base, "f").func == sentinel then return base end end end local function geterroffset(sentinel) local base = getoffset(sentinel) if base then local rv = 0 local _, val = getlocal(base+1, 2) if val == debug and not getlocal(base+1, 3) then rv = rv + 1 end local errfunc = getinfo(base+1+rv, "f").func if errfunc == error or errfunc == assert then rv = rv + 1 end return base - 1, rv end end -- Create the debugger State, which encapsulates everything needed -- during a single invocation of the debugger. This was called -- Context in earlier versions, but that was too confusing. -- -- The State does not persist between calls, so it can't be used -- for things which have to persist (breakpoints, for example). -- Such things could probably go into env; we'll see when I -- actually write the breakpoint handling stuff. -- FIXME There's only one local being protected by this whole -- do block, now that I've hiked statemeta out of it. But -- reindenting will create too big a diff for now. Also, -- change statemeta to statemethods. local statemeta = {}; statemeta.__index = statemeta local State do -- Methods on the state object -- The double indirection here is to allow run-time customization -- of the functions. Maybe it's dumb... -- -- CUSTOMIZABLE CONFIGURATION FUNCTIONS -- We reflect the configurable functions into the state object, -- throwing away self (in most cases) to make the state object's -- interface consistent. -- -- input(prompt) should display prompt to the user and return one -- line of input. function statemeta:input(prompt) return self.var.input(prompt) end -- output should display its arguments in a fashion similar to -- print(). There is no guarantee about the arguments; they may -- be simple values or multi-line help-text. function statemeta:output(...) return self.var.output(...) end -- error should display its argument in a fashion similar to print. -- Indeed, it's possible to use the same function for output and -- error; however, it may be desirable to distinguish style in -- some way. function statemeta:error(...) self.var.error(...) if not self.ignore_error then return false end end -- edit is called to implement the edit command. -- IMPORTANT: edit must return true if it handled the edit; otherwise, -- false and an error message. If nothing or only false is returned, -- ldb will produce a "no editor configured" message. function statemeta:edit(name, line) return self.var.edit(name, line) end -- enterframe is called whenever the debugger enters a different call frame function statemeta:enterframe() return self.var.enterframe(self) end -- leave is called with a value when control is about to return to the program -- the value should be returned as an argument function statemeta:leave(val) return self.var.leave(self, val) end -- id is called to produce an id line for a stackframe. function statemeta:id(info) return self.var.id(info) end -- END OF CUSTOMIZABLE INTERFACE -- Get the offset using getoffset and the cached erroffset function statemeta:getoffset() return getoffset(self.sentinel) + self.erroffset - 1 end -- This is used to read expressions, chunks, etc. from commands function statemeta:getchunk(cmd) local chunk, err = loadstring(cmd, "arg") if chunk then return true, chunk elseif self.interactive and err:match"<eof>" then local more = self:input(self.var.PROMPT2) if more then return self:getchunk(cmd.."\n"..more) end end return false, err end function statemeta:do_in_context(flag, chunk) if flag then setfenv(chunk, self.context.var or {}) return pcall(chunk) else return false, chunk end end -- Announce where we are in the callframe function statemeta:announce() self:enterframe() self:output( ("*%2d %s"):format(self.here.level, self.here.id) ) end -- The proxy stack contains information about the current stack -- (all of it :) ) It probably should be constructed more lazily, -- and with more caching. Otherwise, debugging deep stack frames -- will be painful. -- The stack is indexed by 'level'; currently, that's the same as -- the Lua level, but it might not hurt to remove empty tailcall -- frames from the level count. Each level contains the full -- information returned by debug.getinfo (except available -- breakpoints), plus some computed information. -- We export the stack as a (memoised) array of proxy info -- objects, which are defined here. The proxy object can be -- indexed by the fixed info fields (the complete list of -- available fields is in infokeys at the top of this file); any -- other key is resolved in the context of the callframe. (If an -- infokey collides with a variable, use .env.varname to get at -- the actual variable name.) The proxy objects understand simple -- addition and subtraction as though they were C pointers. It -- seemed convenient. -- Uses an explicit argument name to avoid confusion with the -- meta methods below. function statemeta.proxystack(state) local stack -- defined below local meta = {} local function StackFrame() return setmetatable({}, meta) end function meta:__add(offset) if type(offset) == "number" then return stack[stack[self] + offset] end end function meta:__sub(offset) if type(offset) == "number" then return stack[stack[self] - offset] end end function meta:__index(key) local info = state[stack[self]] if infokeys[key] then return info[key] elseif infofuncs[key] then return infofuncs[key](info) else return info.var[key] end end function meta:__newindex(key, val) local info = state[stack[self]] if infokeys[key] or infofuncs[key] then error(key.." cannot be modified") else info.var[key] = val end end stack = Memoize(function(level) if type(level) == "number" and state[level] then local rv = StackFrame() stack[rv] = level return rv end end) return stack end -- This is the "debugger context", which can be used as an -- alternative to the local context. A few debugger internals -- are exported. -- -- For historical reasons, the context is actually `state.var`, -- which matches the current context being `state.here.var`. It -- might have been better to have called it `context` -- -- Any other key is resolved in the `env` table and then in the -- globals table, but setting is always done in the `env` table. -- -- To expose new internals, you must define both a getter and a -- setter; readonly internals should have an empty setter (although -- I suppose there's nothing wrong with throwing an error). This -- can also be used to "lock" settings in the `env` table as is -- done with `g` (to avoid typos, mostly). -- function statemeta:makecontext(env) local getter, setter = {}, {} function getter.here() return self.here end function getter.stack() return self.stack end function getter.g() return env.g end function setter.here(new) local info = self.stack[new] if type(info) == "number" then self.here = new elseif info then self.here = info end end function setter.stack() end function setter.g() end local meta = {} function meta:__index(key) local g = getter[key] if g then return g() else return env[key] end end function meta:__newindex(key, val) local s = setter[key] if s then s(val) else env[key] = val end end return setmetatable({}, meta) end -- This function builds the local context from information -- hopefully cached by State. -- -- TODO Construct the context lazily local function makeinfovar(info, sentinel, erroffset) info.var = setmetatable({}, { __index = function(_, key) local name, val local index = info.visible[key] if index then if index < 0 then name, val = getupvalue(info.func, -index) else name, val = getlocal(getoffset(sentinel) + erroffset + info.level, index) end else local word, index = key:match"^(%l+)(%d+)$" if word == "upval" then name, val = getupvalue(info.func, tonumber(index)) elseif word == "local" then name, val = getlocal(getoffset(sentinel) + erroffset + info.level, tonumber(index)) end if not name then val = getfenv(info.func)[key] end end return val end, __newindex = function(_, key, val) local index = info.visible[key] if index then if index < 0 then setupvalue(info.func, -index, val) else setlocal(getoffset(sentinel) + erroffset + info.level, index, val) end else local word, index = key:match"^(%l+)(%d+)$" if word == "upval" then setupvalue(info.func, tonumber(index), val) elseif word == "local" then setlocal(getoffset(sentinel) + erroffset + info.level, tonumber(index), val) else getfenv(info.func)[key] = val end end end }) end function State(sentinel, env) local base, erroffset = geterroffset(sentinel) --[[ DEBUG print ("State", base, erroffset) --]] local self if base then base = base + erroffset self = setmetatable({sentinel = sentinel, erroffset = erroffset}, statemeta) self.stack = self:proxystack() self.var = self:makecontext(env) for level = 1, 1e6 do local info = getinfo(base + level, "nSluf") if not info then break end self[level] = info info.level = level local func = info.func if func then local visible = {} if info.what ~= "C" then for index = 1, 1000 do local name = getupvalue(func, index) if name == nil then break end visible[name] = -index end for index = 1, 1000 do local name = getlocal(base + level, index) if name == nil then break end if name:match"^[%a_]" then visible[name] = index end end end info.visible = visible makeinfovar(info, sentinel, erroffset) end -- if func (not a tailcall) end -- for level self.here = self.stack[1] end -- if base return self end end -- SPLAIN -- Command parser and help system --------------------------------- -- Any unambiguous prefix of a command is allowed. Aliases must be -- typed exactly, but may be otherwise ambiguous local resolve_command = Memoize(function (cmd) if command[cmd] then return cmd end for real, short in pairs(alias) do if cmd == short then return real end end local possible for real in pairs(command) do if cmd == real:sub(1, #cmd) then if possible then return "Ambiguous" end possible = real end end return possible or "Unknown" end) function statemeta:do_a_command(input) local bang, dot, args = input:match"%s*(!?)%s*(%.?)%s*(.*)" if args ~= "" then local cmd = args:match"^%W" if cmd then if args:sub(2,2) == cmd then dot = cmd args = args:sub(3) else args = args:sub(2) end else cmd, args = args:match"(%S+)%s*(.*)" if args:sub(1,1) == '=' then args = cmd .. " " .. args:match".%s*(.*)" cmd = "set" end end self.context = dot == "" and self.here or self self.ignore_error = bang == "!" return command[resolve_command[cmd]](self, args) end end -- These save a couple of tests. function command:Ambiguous() return self:error "Ambiguous command" end function command:Unknown() return self:error "Huh?" end -- CORE COMMANDS ------------------------------------------------------------ splain.backtrace = "- print a backtrace" alias.backtrace = "bt" function command:backtrace() local l = self.here.level for i, info in ipairs(self) do self:output((i == l and "*%2d %s" or "%3d %s"):format(i, self:id(info))) end end splain.up = "[N] - move up N levels, default 1" alias.up = 'u' function command:up(i) local here = self.here + (tonumber(i) or 1) if here then self.here = here end end splain.down = "[N] - move down N levels, default 1" alias.down = 'd' function command:down(i) local here = self.here - (tonumber(i) or 1) if here then self.here = here end end splain.edit = [[- open the file of the current context in an external editor For this command to work, `edit` must be defined in ldb-config. See the `ldb-config.example` file in the distribution for examples and more information. ]] function command:edit(filename) local lineno if filename == "" then filename = self.here.source:match"^@(.*)" lineno = self.here.currentline or 1 end if filename then if not self:edit(filename, lineno) then return self:error "No editor configured" end else return self:error "No current file" end end local function safestring(s) s = s:gsub("[%z\1-\31]", function(c) return ("\\%2x"):format(c:byte()) end) if #s > 40 then s = s:sub(1, 37).."..." end return "'"..s.."'" end local function safeshow(x) return (type(x) == "string" and safestring or tostring)(x) end splain.show = [[- show all locals and upvalues at the current level Locals and upvalues which are shadowed are marked with an exclamation point (!). ]] function command:show(cmd) local maxclocals = tonumber(cmd) or 40 local currentlevel = self.here.level local info = self[currentlevel] if info then local level = self:getoffset() + currentlevel local func = info.func if func == nil then self:output"[tailcall]" elseif info.what == "C" then for index = 1, 1000 do local name, val = getupvalue(func, index) if name == nil then break end if index == 1 then self:output"Upvalues:" end self:output( ("%4d: %s"):format(index, safeshow(val)) ) end for index = 1, maxclocals do local name, val = getlocal(level, index) if name == nil then break end if index == 1 then self:output"Locals:" end self:output ( ("%4d: %s"):format(index, safeshow(val)) ) end else local visible = info.visible local upvalue = {} for index = 1, 1000 do local name, val = getupvalue(func, index) if name == nil then break end local vis = index == -visible[name] and " " or "!" upvalue[#upvalue+1] = ("%s %-12s: %s"):format(vis, name, safeshow(val)) end if upvalue[1] then self:output"Upvalues:" table.sort(upvalue) for _, l in ipairs(upvalue) do self:output(l) end end for index = 1, 1000 do local name, val = getlocal(level, index) if name == nil then break end if index == 1 then self:output"Locals:" end local vis = index == visible[name] and " " or "!" self:output( ("%s%3d %-12s: %s"):format(vis, index, name, safeshow(val)) ) end end end end splain.set = [[VAR EXPR - set debugger variable VAR to EXPR EXPR is evaluated in the indicated context (that is, the debugger context if invoked as `.set` and otherwise the current context. The named VAR (which currently must be a simple name) is set in the debugger context. You cannot use this command to set global variables. Normally you don't need to use the `set` command, because ldb interprets any command line whose second token is `=` as a set command. For example, the following are equivalent: foo = getmetamethod(local1) set foo getmetamethod(local1) and the following three commands are equivalent: .here = here+1 .set here here+1 ,,here=here + 1 Note that the `=` must be preceded by a space to be recognized as a `set` command. ]] function command:set(cmd) local var, chunk = cmd:match"(%S+)%s*(.*)" if not var:match"^[%a_][%w_]*$" then return self:error "Only simple variable names can be set" elseif chunk == "" then return self:error "No value specified for set" else local ok, val = self:do_in_context(self:getchunk("return "..chunk)) if ok then self.var[var] = val else return self:error(val) end end end splain.print = [[EXPR - evaluate and print EXPR If invoked as `print` or `=`, the EXPR is evaluated in the current context. If invoked as `.print` or `==`, the EXPR is evaluated in the debugger context. See `help context` for more information. ]] alias.print = '=' function command:print(cmd) self:output(select(2, self:do_in_context(self:getchunk("return "..cmd)))) end splain.exec = [[CHUNK - evaluate CHUNK If invoked as `exec` or `,`, the CHUNK is evaluated in the current context. If invoked as `.exec` or `..` the CHUNK is evaluated in the debugger context. See `help context` for more information. ]] alias.exec = "," function command:exec(cmd) local ok, err = self:do_in_context(self:getchunk(cmd)) if ok then if self.interactive then self:output"Ok!" end else return self:error(err) end end splain.quit = [=[[EXPR] - return from ldb, with an optional value Causes the current invocation of ldb to return. If an EXPR is provided, it will be returned from the call to ldb (which is only useful if ldb is invoked directly). Currently, only a single value may be returned. If the evaluation of EXPR throws an error and the command was not being executed interactively, ldb will return <false, error message> ]=] function command:quit(cmd) if cmd:match"%S" then local ok, val = self:do_in_context(self:getchunk("return "..cmd)) if ok then return true, val elseif not self.interactive then return true, false, val else return self:error(val) end end return true end splain["if"] = [=[EXPR - return from ldb if EXPR is a true value Causes the current invocation of ldb to return with value EXPR if EXPR evaluates to a value other than `false` or `nil`. If an error is thrown, the invocation does not return. ]=] command["if"] = function(self, cmd) local ok, val = self:do_in_context(self:getchunk("return "..cmd)) if ok and val then return ok, val end end splain.unless = [=[EXPR - return from ldb if EXPR is `false` or `nil` Causes the current invocation of ldb to return (with no return value) if EXPR evalutes to `false` or `nil`, or throws an error. ]=] function command:unless(cmd) local ok, val = self:do_in_context(self:getchunk("return "..cmd)) return not ok or not val end splain.continue = [[- return from ldb]] alias.continue = 'c' function command:continue() return true end -- Topics splain.intro = [[* quick introduction to ldb ldb is a very basic interactive debugger. Currently it only allows stack inspection, although breakpoints and watches will be supported in the future. The usual way of setting ldb up is: ldb = require"ldb" debug.traceback = ldb This will trigger ldb when any error occurs. You can also call ldb directly in your code, as a sort of manual breakpoint. There are only a few commands (so far) and all of them are documented in the help system. Take a few minutes to review them. The most common ones are: bt print a backtrace = print the value of an expression in the current context == print the value of an expression in the debug context , execute a statement in the current context ,, execute a statement in the debug context show show all of the variables in the current context Read `help context` for an explanation of contexts. It's really important. ]] splain.context = [[- how contexts work in ldb A context is essentially a variable scope; the combination of (visible) locals, upvalues and "globals" (i.e. the function environment.) ldb always has two contexts: the current context and the debugger context. When ldb is called, it sets the current context to the context of the caller, or the point at which an error was thrown, if ldb is being used as an error function (such as debug.traceback). You can move up and down in the call stack with the `up` and `down` commands; as you move, the current context will shift to reflect where you are. ldb also has its own context, which is persistent between invocations of ldb. You can set values in this context which will be available on subsequent invocations of the debugger. The ldb context also contains ldb configuration variables, such as `PROMPT` and `output`, and `g`, which refers to the global environment in which `ldb` was originally loaded. There are also a few 'magic' variables. ldb commands which take an expression or a chunk as an argument evaluate the expression or chunk in the current context by default. If you prefix the command with `.`, then the debugger context will be used instead. For convenience, `.,` can be typed as `,,` and `.=` can be typed as `==`. For more details, see `help magic`, `help here`, `help clocal` and `help tailcall`. ]] splain.clocal = [[- debugging inside C frames In C frames, there are no named variables -- ldb doesn't integrate with gdb, unfortunately -- but the stack and upvalues are still available as numbered variables in the form `local#` and `upval#` where # is a decimal number, possibly with leading zeros. For example, `local1` is stack slot 1 in the C frame, and `upval1` is the first upvalue (if there is one). You can use these as though they were ordinary variables. You can also use this format to get at shadowed locals and in a Lua frame. Note that the variable is first looked up in the context, so you if you have a local variable called `local1`, you would need to use `local01` to get at the first stack slot. Another issue you may run into with C frames is that ldb uses the CFunction's environment, not the thread environment, as the environment when the current context is a C frame. Most C functions operate on `LUA_GLOBALSINDEX` rather than `LUA_ENVIRONINDEX`, so the results might be surprising. ]] splain.tailcall = [[- gotchas with tailcall frames The Lua debug interface includes the concept of 'deleted' tailcall stack frames; i.e. the frames which have been recycled by a tail call of the form: return other_func() These deleted frames have no information -- they really have been deleted -- but they show up in backtraces anyway. At some point this might irritate me enough to remove them from the backtraces, but for now they are there. Deleted frames have any empty context. There are no variables, not even standard library functions. If you get an error that some global which obviously exists is `nil`, you should make sure that the current frame is not a tailcall frame. (ldb doesn't insert "the" globals table, because it's not at all clear what that would mean -- had the frame not been deleted, the function would have had an environment table which might well not be the "global" environment.) ]] splain.magic = [[- 'magic' fields in the ldb context ldb maintains its own environment, which you can use as a scratchpad; assignments to the ldb environment persist between calls to ldb, but do not affect the global environment. For convenience, the ldb environment has an __index metamethod which provides access to the initial global environment, but the __newindex metamethod does not direct modification of the global environment. The global environment is available as the variable `g` in the ldb environment, so you can set a global variable like this: (ldb) ,, g.some_global = 42 Here is a list of all 'magic' variables in the ldb context: here information about the current stack frame. See `help here` for details. g (RO) The value of _G when the debugger was entered stack (RO) An array of stack frame info objects, as above. stack[here.level] == here PROMPT The debugger comand prompt PROMPT2 The debugger continuation prompt ]] splain.here = [[- the 'here' field in the ldb context The variable `here` in the ldb context is a reference to the 'current' stack frame. When ldb is invoked the current stackframe is the place where ldb was invoked (either directly or via an error function); you can view the stack with `backtrace` (usually written `bt`, in which the current stackframe is marked with a '*', and you can move up and down in the stack with the `up` and `down` commands. `here` contains all of the fields returned by `debug.getinfo` except for the table of valid breakpoints. It also contains some variables added by ldb itself. See `help getinfo` for a list. `here` is special; it can be added to or subtracted from to get at other levels. So instead of writing (ldb) == stack[here.level - 1].foo (to see the value of 'foo' in the calling frame), you can simply type: (ldb) == (here-1).foo You can assign `here` to any valid stack frame, or to the number of a valid stack frame, allowing you to navigate in the stack. For example, (ldb) ,, here = here + 1 is exactly the same as the `up` command, and: (ldb) ,, here = 7 takes you to stackframe 7 directly. ]] splain.getinfo = [[- fields in the getinfo table The getinfo fields are described in the Lua reference manual, although ldb adds a few of its own. Here is a complete list: currentline The line currently being executed by the function fenv The function's environment (that is, getfenv(func) ) func The function itself id The id line ldb uses to identify the stackframe linedefined The line in the source where the function is defined lastlinedefined The line where the definition of the function ends level Level of the frame in the call stack name A possible name for the function, if Lua can figure it out. This will be `nil` in a stackframe discarded by a tailcall; otherwise it will be the empty string if Lua can't find a name. namewhat "global", "local", "method", "field", "upvalue" or "" nups The number of upvalues of the function short_src A shorter version of `source` source `@` followed by the filename, or "=stdin", or whatever was provided as the last argument to loadfile var The stackframe context; locals, upvalues and the fenv are reflected into this table. You can also use local# and upval# (where # is any integer) to get at locals and upvalues by number. Assignment is allowed (and will be to the fenv if there is no local or upvalue with the given name.) what "Lua", "C", "main" or "tail" ]] splain.copyright = [[* ldb is public domain ldb was originally written by Rici Lake, and released into the public domain. Do with it as you see fit. The author disclaims all rights and responsibilities. ]] splain.version = [[* this is version]]..VERSION.."-"..VERSION_DATE splain.help = "[CMD] - show help for CMD or all commands" alias.help = "?" do local function sorted(filter) local t = {} for k, v in pairs(splain) do if filter(k, v) then t[#t+1] = k end end table.sort(t) local i = 0 return function() i = i + 1; if t[i] then return t[i], splain[t[i]] end end end local function align(col1, col2) local intro = "" if #col1 > 15 then intro, col1 = col1.."\n", intro end return ("%s%-16s %s"):format(intro, col1, col2) end function command:help(cmd) cmd = cmd:gsub("%s", "") if cmd == "topics" then self:output"topics - help <topic> for details" self:output"" for key, descr in sorted( function(key, descr) return not command[key] and descr:sub(1,1) == "*" end ) do self:output(align(key, descr:match"^%*[ \t]*([^\n]*)")) end self:output"" for key, descr in sorted( function(key, descr) return not command[key] and descr:sub(1,1) ~= "*" end ) do self:output(align(key, descr:match"^.[ \t]*([^\n]*)")) end elseif cmd == "" then for cmd, descr in sorted(function(key) return command[key] end) do local arg, desc = descr:match"^([^-\n]-) ?%- ([^\n]*)" self:output(align( ("%-3s%s"):format(alias[cmd] or "", cmd.." "..arg), desc) ) end self:output"\nuse `help topics` for help on particular issues" elseif splain[cmd] then self:output(cmd.." "..splain[cmd]) else local real = resolve_command[cmd] self:output(cmd.." ("..real..") "..(splain[real] or "")) end end end -- Do a single command and return function handler:ldb_cmd(token, cmd) self:do_a_command(cmd) return true end function env.ldb (arg1, arg2) local check = prehandler[arg1] if check then local newarg1, newarg2 = check(arg2) if not newarg1 then return end arg1 = newarg1; arg2 = newarg2 end local function sentinel(arg1, arg2) local self = State(sentinel, env) assert(self, "Couldn't find myself in the backtrace!") self.var.arg1 = arg1 self.var.arg2 = arg2 local f = handler[arg1] if f then local flag, val = f(self, arg1, arg2) if flag then return self:leave(val) end else if type(arg2) == "string" then local flag, val = self:do_a_command(arg2) if flag then return self:leave(val) end elseif type(arg2) == "table" then for _, cmd in ipairs(arg2) do local flag, val = self:do_a_command(cmd) if flag ~= nil then return self:leave(val) end end end if arg1 then self:output(arg1) end end self.interactive = true local here while true do if here ~= self.here then self:announce() here = self.here end local flag, val = self:do_a_command(self:input(env.PROMPT) or "continue") if flag then return self:leave(val) end end end -- the tailcall will put a pseudoframe on the stack, but it -- should still be skipped over by getinfo return sentinel(arg1, arg2) end -- In case we were started in some other way than "require'ldb'": package.loaded.ldb = env.ldb env.load_plugins() return env.ldb