aboutsummaryrefslogtreecommitdiff
path: root/.mbs/lib/stack_trace.lua
blob: 2c94dcde1426bc3e55788d8cce15c2ab99117843 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
local type = type
local debug_traceback = type(debug) == "table" and type(debug.traceback) == "function" and debug.traceback

local function traceback(x)
  -- Attempt to detect error() and error("xyz", 0).
  -- This probably means they're erroring the program intentionally and so we
  -- shouldn't display anything.
  if x == nil or type(x) == "string" and not x:find(":%d+:") then
    return x
  end

  if debug_traceback then
    -- The parens are important, as they prevent a tail call occuring, meaning
    -- the stack level is preserved. This ensures the code behaves identically
    -- on LuaJ and PUC Lua.
    return (debug_traceback(tostring(x), 2))
  else
    local level = 3
    local out = { tostring(x), "stack traceback:" }
    while true do
      local _, msg = pcall(error, "", level)
      if msg == "" then break end

      out[#out + 1] = "  " .. msg
      level = level + 1
    end

    return table.concat(out, "\n")
  end
end

local function trim_traceback(target, marker)
  local ttarget, tmarker = {}, {}
  for line in target:gmatch("([^\n]*)\n?") do ttarget[#ttarget + 1] = line end
  for line in marker:gmatch("([^\n]*)\n?") do tmarker[#tmarker + 1] = line end

  -- Trim identical suffixes
  local t_len, m_len = #ttarget, #tmarker
  while t_len >= 3 and ttarget[t_len] == tmarker[m_len] do
    table.remove(ttarget, t_len)
    t_len, m_len = t_len - 1, m_len - 1
  end

  -- Trim elements from this file and xpcall invocations
  while t_len >= 1 and ttarget[t_len]:find("^\tstack_trace%.lua:%d+:") or
        ttarget[t_len] == "\t[C]: in function 'xpcall'" or ttarget[t_len] == "  xpcall: " do
    table.remove(ttarget, t_len)
    t_len = t_len - 1
  end

  return ttarget
end

--- Run a function with
local function xpcall_with(fn)
  -- So this is rather grim: we need to get the full traceback and current one and remove
  -- the common prefix
  local trace
  local res = table.pack(xpcall(fn, traceback)) if not res[1] then trace = traceback("trace.lua:1:") end
  local ok, err = res[1], res[2]

  if not ok and err ~= nil then
    trace = trim_traceback(err, trace)

    -- Find the position where the stack traceback actually starts
    local trace_starts
    for i = #trace, 1, -1 do
      if trace[i] == "stack traceback:" then trace_starts = i break end
    end

    -- If this traceback is more than 15 elements long, keep the first 9, last 5
    -- and put an ellipsis between the rest
    local max = 15
    if trace_starts and #trace - trace_starts > max then
      local keep_starts = trace_starts + 10
      for i = #trace - trace_starts - max, 0, -1 do table.remove(trace, keep_starts + i) end
      table.insert(trace, keep_starts, "  ...")
    end

    return false, table.concat(trace, "\n")
  end

  return table.unpack(res, 1, res.n)
end

_ENV.traceback = traceback
_ENV.trim_traceback = trim_traceback
_ENV.xpcall_with = xpcall_with