aboutsummaryrefslogtreecommitdiff
path: root/src/StackTracePlus.h
diff options
context:
space:
mode:
Diffstat (limited to 'src/StackTracePlus.h')
-rw-r--r--src/StackTracePlus.h550
1 files changed, 550 insertions, 0 deletions
diff --git a/src/StackTracePlus.h b/src/StackTracePlus.h
new file mode 100644
index 0000000..1a909fd
--- /dev/null
+++ b/src/StackTracePlus.h
@@ -0,0 +1,550 @@
1R"lua_codes(
2--[[
3The MIT License
4
5Copyright (c) 2010 Ignacio BurgueƱo
6
7Permission is hereby granted, free of charge, to any person obtaining a copy
8of this software and associated documentation files (the "Software"), to deal
9in the Software without restriction, including without limitation the rights
10to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11copies of the Software, and to permit persons to whom the Software is
12furnished to do so, subject to the following conditions:
13
14The above copyright notice and this permission notice shall be included in
15all copies or substantial portions of the Software.
16
17THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23THE SOFTWARE.]]
24
25-- tables
26local _G = _G
27local string, io, debug, coroutine = string, io, debug, coroutine
28
29-- functions
30local tostring, require = tostring, require
31local next, assert = next, assert
32local pcall, type, pairs, ipairs = pcall, type, pairs, ipairs
33local error = error
34
35assert(debug, "debug table must be available at this point")
36
37local string_gmatch = string.gmatch
38local string_sub = string.sub
39local table_concat = table.concat
40
41local _M = {
42 max_tb_output_len = 70, -- controls the maximum length of the 'stringified' table before cutting with ' (more...)'
43 dump_locals = true,
44 simplified = false
45}
46
47-- this tables should be weak so the elements in them won't become uncollectable
48local m_known_tables = { [_G] = "_G (global table)" }
49local function add_known_module(name, desc)
50 local ok, mod = pcall(require, name)
51 if ok then
52 m_known_tables[mod] = desc
53 end
54end
55
56add_known_module("string", "string module")
57add_known_module("io", "io module")
58add_known_module("os", "os module")
59add_known_module("table", "table module")
60add_known_module("math", "math module")
61add_known_module("package", "package module")
62add_known_module("debug", "debug module")
63add_known_module("coroutine", "coroutine module")
64
65-- lua5.2
66add_known_module("bit32", "bit32 module")
67-- luajit
68add_known_module("bit", "bit module")
69add_known_module("jit", "jit module")
70-- lua5.3
71if _VERSION >= "Lua 5.3" then
72 add_known_module("utf8", "utf8 module")
73end
74
75
76local m_user_known_tables = {}
77
78local m_known_functions = {}
79for _, name in ipairs{
80 -- Lua 5.2, 5.1
81 "assert",
82 "collectgarbage",
83 "dofile",
84 "error",
85 "getmetatable",
86 "ipairs",
87 "load",
88 "loadfile",
89 "next",
90 "pairs",
91 "pcall",
92 "print",
93 "rawequal",
94 "rawget",
95 "rawlen",
96 "rawset",
97 "require",
98 "select",
99 "setmetatable",
100 "tonumber",
101 "tostring",
102 "type",
103 "xpcall",
104
105 -- Lua 5.1
106 "gcinfo",
107 "getfenv",
108 "loadstring",
109 "module",
110 "newproxy",
111 "setfenv",
112 "unpack",
113 -- TODO: add table.* etc functions
114} do
115 if _G[name] then
116 m_known_functions[_G[name]] = name
117 end
118end
119
120local m_user_known_functions = {}
121
122local function safe_tostring (value)
123 local ok, err = pcall(tostring, value)
124 if ok then return err else return ("<failed to get printable value>: '%s'"):format(err) end
125end
126
127-- Private:
128-- Parses a line, looking for possible function definitions (in a very naive way)
129-- Returns '(anonymous)' if no function name was found in the line
130local function ParseLine(line)
131 assert(type(line) == "string")
132 local match = line:match("^%s*function%s+(%w+)")
133 if match then
134 --print("+++++++++++++function", match)
135 return match
136 end
137 match = line:match("^%s*local%s+function%s+(%w+)")
138 if match then
139 --print("++++++++++++local", match)
140 return match
141 end
142 match = line:match("^%s*local%s+(%w+)%s+=%s+function")
143 if match then
144 --print("++++++++++++local func", match)
145 return match
146 end
147 match = line:match("%s*function%s*%(") -- this is an anonymous function
148 if match then
149 --print("+++++++++++++function2", match)
150 return "(anonymous)"
151 end
152 return "(anonymous)"
153end
154
155-- Private:
156-- Tries to guess a function's name when the debug info structure does not have it.
157-- It parses either the file or the string where the function is defined.
158-- Returns '?' if the line where the function is defined is not found
159local function GuessFunctionName(info)
160 -- print("guessing function name")
161 if type(info.source) == "string" and info.source:sub(1,1) == "@" then
162 local file = io.open(info.source:sub(2))
163 local text
164 if file then
165 text = file:read("*a")
166 file:close()
167 end
168 if not text then
169 -- print("file not found: "..tostring(err)) -- whoops!
170 return "?"
171 end
172 local line
173 local count = 0
174 for lineText in (text.."\n"):gmatch("(.-)\n") do
175 line = lineText
176 count = count + 1
177 if count == info.linedefined then
178 break
179 end
180 end
181 if not line then
182 --print("line not found") -- whoops!
183 return "?"
184 end
185 return ParseLine(line)
186 else
187 local line
188 local lineNumber = 0
189 for l in string_gmatch(info.source, "([^\n]+)\n-") do
190 lineNumber = lineNumber + 1
191 if lineNumber == info.linedefined then
192 line = l
193 break
194 end
195 end
196 if not line then
197 -- print("line not found") -- whoops!
198 return "?"
199 end
200 return ParseLine(line)
201 end
202end
203
204---
205-- Dumper instances are used to analyze stacks and collect its information.
206--
207local Dumper = {}
208
209Dumper.new = function(thread)
210 local t = { lines = {} }
211 for k,v in pairs(Dumper) do t[k] = v end
212
213 t.dumping_same_thread = (thread == coroutine.running())
214
215 -- if a thread was supplied, bind it to debug.info and debug.get
216 -- we also need to skip this additional level we are introducing in the callstack (only if we are running
217 -- in the same thread we're inspecting)
218 if type(thread) == "thread" then
219 t.getinfo = function(level, what)
220 if t.dumping_same_thread and type(level) == "number" then
221 level = level + 1
222 end
223 return debug.getinfo(thread, level, what)
224 end
225 t.getlocal = function(level, loc)
226 if t.dumping_same_thread then
227 level = level + 1
228 end
229 return debug.getlocal(thread, level, loc)
230 end
231 else
232 t.getinfo = debug.getinfo
233 t.getlocal = debug.getlocal
234 end
235
236 return t
237end
238
239-- helpers for collecting strings to be used when assembling the final trace
240function Dumper:add (text)
241 self.lines[#self.lines + 1] = text
242end
243function Dumper:add_f (fmt, ...)
244 self:add(fmt:format(...))
245end
246function Dumper:concat_lines ()
247 return table_concat(self.lines)
248end
249
250---
251-- Private:
252-- Iterates over the local variables of a given function.
253--
254-- @param level The stack level where the function is.
255--
256function Dumper:DumpLocals (level)
257 if not _M.dump_locals then return end
258
259 local prefix = "\t "
260 local i = 1
261
262 if self.dumping_same_thread then
263 level = level + 1
264 end
265
266 local name, value = self.getlocal(level, i)
267 if not name then
268 return
269 end
270 self:add("\tLocal variables:\r\n")
271 while name do
272 if type(value) == "number" then
273 self:add_f("%s%s = number: %g\r\n", prefix, name, value)
274 elseif type(value) == "boolean" then
275 self:add_f("%s%s = boolean: %s\r\n", prefix, name, tostring(value))
276 elseif type(value) == "string" then
277 self:add_f("%s%s = string: %q\r\n", prefix, name, value)
278 elseif type(value) == "userdata" then
279 self:add_f("%s%s = %s\r\n", prefix, name, safe_tostring(value))
280 elseif type(value) == "nil" then
281 self:add_f("%s%s = nil\r\n", prefix, name)
282 elseif type(value) == "table" then
283 if m_known_tables[value] then
284 self:add_f("%s%s = %s\r\n", prefix, name, m_known_tables[value])
285 elseif m_user_known_tables[value] then
286 self:add_f("%s%s = %s\r\n", prefix, name, m_user_known_tables[value])
287 else
288 local txt = "{"
289 for k,v in pairs(value) do
290 txt = txt..safe_tostring(k)..":"..safe_tostring(v)
291 if #txt > _M.max_tb_output_len then
292 txt = txt.." (more...)"
293 break
294 end
295 if next(value, k) then txt = txt..", " end
296 end
297 self:add_f("%s%s = %s %s\r\n", prefix, name, safe_tostring(value), txt.."}")
298 end
299 elseif type(value) == "function" then
300 local info = self.getinfo(value, "nS")
301 local fun_name = info.name or m_known_functions[value] or m_user_known_functions[value]
302 if info.what == "C" then
303 self:add_f("%s%s = C %s\r\n", prefix, name, (fun_name and ("function: " .. fun_name) or tostring(value)))
304 else
305 local source = info.short_src
306 if source:sub(2,7) == "string" then
307 source = source:sub(9)
308 end
309 --for k,v in pairs(info) do print(k,v) end
310 fun_name = fun_name or GuessFunctionName(info)
311 self:add_f("%s%s = Lua function '%s' (defined at line %d of chunk %s)\r\n", prefix, name, fun_name, info.linedefined, source)
312 end
313 elseif type(value) == "thread" then
314 self:add_f("%sthread %q = %s\r\n", prefix, name, tostring(value))
315 end
316 i = i + 1
317 name, value = self.getlocal(level, i)
318 end
319end
320
321local function getMoonLineNumber(fname, line)
322 local moonCompiled = require("moonp").moon_compiled
323 local source = moonCompiled["@"..fname]
324 local file = io.open(fname)
325 if not source and file then
326 local codes = file:read("*a")
327 file:close()
328 local moonFile = codes:match("^%s*--%s*%[moon%]:%s*([^\n]*)")
329 if moonFile then
330 fname = moonFile:gsub("^%s*(.-)%s*$", "%1")
331 source = codes
332 end
333 end
334 if source then
335 local i, target = 1, tonumber(line)
336 for lineCode in source:gmatch("([^\n]*)\n") do
337 if i == target then
338 local num = lineCode:match("--%s*(%d*)%s*$")
339 if num then
340 return fname, num
341 end
342 break
343 end
344 i = i + 1
345 end
346 end
347 return fname, line
348end
349
350---
351-- Public:
352-- Collects a detailed stack trace, dumping locals, resolving function names when they're not available, etc.
353-- This function is suitable to be used as an error handler with pcall or xpcall
354--
355-- @param thread An optional thread whose stack is to be inspected (defaul is the current thread)
356-- @param message An optional error string or object.
357-- @param level An optional number telling at which level to start the traceback (default is 1)
358--
359-- Returns a string with the stack trace and a string with the original error.
360--
361function _M.stacktrace(thread, message, level)
362 if type(thread) ~= "thread" then
363 -- shift parameters left
364 thread, message, level = nil, thread, message
365 end
366
367 thread = thread or coroutine.running()
368
369 level = level or 1
370
371 local dumper = Dumper.new(thread)
372
373 local original_error
374
375 if type(message) == "table" then
376 dumper:add("an error object {\r\n")
377 local first = true
378 for k,v in pairs(message) do
379 if first then
380 dumper:add(" ")
381 first = false
382 else
383 dumper:add(",\r\n ")
384 end
385 dumper:add(safe_tostring(k))
386 dumper:add(": ")
387 dumper:add(safe_tostring(v))
388 end
389 dumper:add("\r\n}")
390 original_error = dumper:concat_lines()
391 elseif type(message) == "string" then
392 original_error = message
393 local fname, line, msg = message:match('(.+):(%d+): (.*)$')
394 local nfname, nline, nmsg = fname:match('(.+):(%d+): (.*)$')
395 if nfname then
396 fname = nmsg
397 end
398 if fname then
399 fname = fname:gsub("%[string \"", "")
400 fname = fname:gsub("\"%]", "")
401 fname = fname:gsub("^%s*(.-)%s*$", "%1")
402 local extension = fname:match("%.([^%.\\/]*)$")
403 if not extension then
404 local fext = fname .. ".lua"
405 local file = io.open(fext)
406 if file then
407 file:close()
408 fname = fext
409 else
410 fext = fname .. ".moon"
411 file = io.open(fext)
412 if file then
413 file:close()
414 fname = fext
415 end
416 end
417 end
418 fname, line = getMoonLineNumber(fname, line)
419 if _M.simplified then
420 message = table.concat({
421 "'", fname, "':",
422 line, ": ", msg})
423 else
424 message = table.concat({
425 "[string \"", fname, "\"]:",
426 line, ": ", msg})
427 end
428 end
429 dumper:add(message)
430 end
431
432 dumper:add("\r\n")
433 dumper:add[[
434Stack Traceback
435===============
436]]
437 --print(error_message)
438
439 local level_to_show = 1
440 if dumper.dumping_same_thread then level = level + 1 end
441
442 local info = dumper.getinfo(level, "nSlf")
443 while info do
444 if info.source and info.source:sub(1,1) == "@" then
445 info.source = info.source:sub(2)
446 elseif info.what == "main" or info.what == "Lua" then
447 info.source = info.source
448 end
449 local fname = info.source
450 local extension = fname:match("%.([^%.\\/]*)$")
451 if not extension then
452 local fext = fname .. ".lua"
453 local file = io.open(fext)
454 if file then
455 file:close()
456 fname = fext
457 else
458 fext = fname .. ".moon"
459 file = io.open(fext)
460 if file then
461 file:close()
462 fname = fext
463 end
464 end
465 end
466 info.source, info.currentline = getMoonLineNumber(fname, info.currentline)
467 if info.what == "main" then
468 if _M.simplified then
469 dumper:add_f("(%d) '%s':%d\r\n", level_to_show, info.source, info.currentline)
470 else
471 dumper:add_f("(%d) main chunk of file '%s' at line %d\r\n", level_to_show, info.source, info.currentline)
472 end
473 elseif info.what == "C" then
474 --print(info.namewhat, info.name)
475 --for k,v in pairs(info) do print(k,v, type(v)) end
476 local function_name = m_user_known_functions[info.func] or m_known_functions[info.func] or info.name or tostring(info.func)
477 dumper:add_f("(%d) %s C function '%s'\r\n", level_to_show, info.namewhat, function_name)
478 --dumper:add_f("%s%s = C %s\r\n", prefix, name, (m_known_functions[value] and ("function: " .. m_known_functions[value]) or tostring(value)))
479 elseif info.what == "tail" then
480 --print("tail")
481 --for k,v in pairs(info) do print(k,v, type(v)) end--print(info.namewhat, info.name)
482 dumper:add_f("(%d) tail call\r\n", level_to_show)
483 dumper:DumpLocals(level)
484 elseif info.what == "Lua" then
485 local source = info.source
486 local function_name = m_user_known_functions[info.func] or m_known_functions[info.func] or info.name
487 if source:sub(2, 7) == "string" then
488 source = source:sub(10,-3)
489 end
490 local was_guessed = false
491 if not function_name or function_name == "?" then
492 --for k,v in pairs(info) do print(k,v, type(v)) end
493 function_name = GuessFunctionName(info)
494 was_guessed = true
495 end
496 -- test if we have a file name
497 local function_type = (info.namewhat == "") and "function" or info.namewhat
498 if info.source and info.source:sub(1, 1) == "@" then
499 if _M.simplified then
500 dumper:add_f("(%d) '%s':%d%s\r\n", level_to_show, info.source:sub(2), info.currentline, was_guessed and " (guess)" or "")
501 else
502 dumper:add_f("(%d) Lua %s '%s' at file '%s':%d%s\r\n", level_to_show, function_type, function_name, info.source:sub(2), info.currentline, was_guessed and " (best guess)" or "")
503 end
504 elseif info.source and info.source:sub(1,1) == '#' then
505 if _M.simplified then
506 dumper:add_f("(%d) '%s':%d%s\r\n", level_to_show, info.source:sub(2), info.currentline, was_guessed and " (guess)" or "")
507 else
508 dumper:add_f("(%d) Lua %s '%s' at template '%s':%d%s\r\n", level_to_show, function_type, function_name, info.source:sub(2), info.currentline, was_guessed and " (best guess)" or "")
509 end
510 else
511 if _M.simplified then
512 dumper:add_f("(%d) '%s':%d\r\n", level_to_show, source, info.currentline)
513 else
514 dumper:add_f("(%d) Lua %s '%s' at chunk '%s':%d\r\n", level_to_show, function_type, function_name, source, info.currentline)
515 end
516 end
517 dumper:DumpLocals(level)
518 else
519 dumper:add_f("(%d) unknown frame %s\r\n", level_to_show, info.what)
520 end
521
522 level = level + 1
523 level_to_show = level_to_show + 1
524 info = dumper.getinfo(level, "nSlf")
525 end
526
527 return dumper:concat_lines(), original_error
528end
529
530--
531-- Adds a table to the list of known tables
532function _M.add_known_table(tab, description)
533 if m_known_tables[tab] then
534 error("Cannot override an already known table")
535 end
536 m_user_known_tables[tab] = description
537end
538
539--
540-- Adds a function to the list of known functions
541function _M.add_known_function(fun, description)
542 if m_known_functions[fun] then
543 error("Cannot override an already known function")
544 end
545 m_user_known_functions[fun] = description
546end
547
548return _M
549
550)lua_codes";