1 local debug_getinfo = debug.getinfo
2 local Files = require("files")
3 local C = require("colors")
4 local pretty_error = require("pretty_errors")
5 local ok, to_lua = pcall(function()
6 return require('moonscript.base').to_lua
13 local MOON_SOURCE_MAP = setmetatable({ }, {
14 __index = function(self, file)
15 local _, line_table = to_lua(file)
16 self[file] = line_table or false
17 return line_table or false
20 debug.getinfo = function(thread, f, what)
22 f, what, thread = thread, f, nil
24 if type(f) == 'number' then
29 info = debug_getinfo(f, what)
31 info = debug_getinfo(thread, f, what)
33 if not info or not info.func then
36 if info.short_src or info.source or info.linedefine or info.currentline then
38 local map = SOURCE_MAP[info.source]
40 if info.currentline then
41 info.currentline = assert(map[info.currentline])
43 if info.linedefined then
44 info.linedefined = assert(map[info.linedefined])
46 if info.lastlinedefined then
47 info.lastlinedefined = assert(map[info.lastlinedefined])
49 info.short_src = info.source:match('@([^[]*)') or info.short_src
51 info.name = "action '" .. tostring(info.name:from_lua_id()) .. "'"
53 info.name = "main chunk"
61 strdist = function(a, b, cache)
74 local k = a .. '\003' .. b
75 if not (cache[k]) then
76 cache[k] = math.min(strdist(a:sub(1, -2), b, cache) + 1, strdist(a, b:sub(1, -2), cache) + 1, strdist(a:sub(1, -2), b:sub(1, -2), cache) + (a:sub(-1) ~= b:sub(-1) and 1.1 or 0))
77 if #a >= 2 and #b >= 2 and a:sub(-1, -1) == b:sub(-2, -2) and a:sub(-2, -2) == b:sub(-1, -1) then
78 cache[k] = math.min(cache[k], strdist(a:sub(1, -3), b:sub(1, -3), cache) + 1)
84 enhance_error = function(error_message)
85 if not (error_message and error_message:match("%d|")) then
86 error_message = error_message or ""
88 local fn_name = error_message:match("attempt to call a nil value %(method '(.*)'%)")
90 local action_name = fn_name:from_lua_id()
91 error_message = "This object does not have the method '" .. tostring(action_name) .. "'."
94 fn_name = (error_message:match("attempt to call a nil value %(global '(.*)'%)") or error_message:match("attempt to call global '(.*)' %(a nil value%)"))
96 local action_name = fn_name:from_lua_id()
97 error_message = "The action '" .. tostring(action_name) .. "' is not defined."
98 local func = debug.getinfo(2, 'f').func
100 if _VERSION == "Lua 5.1" then
104 ename, env = debug.getupvalue(func, 1)
105 if not (ename == "_ENV" or ename == "_G") then
106 func = debug.getinfo(3, 'f').func
107 ename, env = debug.getupvalue(func, 1)
110 local THRESHOLD = math.min(4.5, .9 * #action_name)
111 local candidates = { }
114 local k, v = debug.getlocal(2, i)
118 if not (k:sub(1, 1) == "(" or type(v) ~= 'function') then
120 if strdist(k, action_name, cache) <= THRESHOLD and k ~= "" then
121 table.insert(candidates, k)
125 for i = 1, debug.getinfo(func, 'u').nups do
126 local k, v = debug.getupvalue(func, i)
127 if not (k:sub(1, 1) == "(" or type(v) ~= 'function') then
129 if strdist(k, action_name, cache) <= THRESHOLD and k ~= "" then
130 table.insert(candidates, k)
135 scan = function(t, is_lua_id)
139 for k, v in pairs(t) do
140 if type(k) == 'string' and type(v) == 'function' then
141 if not (is_lua_id) then
144 if strdist(k, action_name, cache) <= THRESHOLD and k ~= "" then
145 table.insert(candidates, k)
150 scan(env.COMPILE_RULES, true)
151 scan(env.COMPILE_RULES._IMPORTS, true)
154 if #candidates > 0 then
155 for _index_0 = 1, #candidates do
156 local c = candidates[_index_0]
157 THRESHOLD = math.min(THRESHOLD, strdist(c, action_name, cache))
162 for _index_0 = 1, #candidates do
163 local c = candidates[_index_0]
164 if strdist(c, action_name, cache) <= THRESHOLD then
169 candidates = _accum_0
171 if #candidates == 1 then
172 error_message = error_message .. "\n\x1b[3mSuggestion: Maybe you meant '" .. tostring(candidates[1]) .. "'? "
173 elseif #candidates > 0 then
174 local last = table.remove(candidates)
175 error_message = error_message .. ("\n" .. C('italic', "Suggestion: Maybe you meant '" .. tostring(table.concat(candidates, "', '")) .. "'" .. tostring(#candidates > 1 and ',' or '') .. " or '" .. tostring(last) .. "'? "))
184 local calling_fn = debug_getinfo(level)
185 if not calling_fn then
189 local filename, file, line_num
191 local map = SOURCE_MAP and SOURCE_MAP[calling_fn.source]
193 if calling_fn.currentline then
194 line_num = assert(map[calling_fn.currentline])
197 filename, start, stop = calling_fn.source:match('@([^[]*)%[([0-9]+):([0-9]+)]')
199 filename, start = calling_fn.source:match('@([^[]*)%[([0-9]+)]')
202 file = Files.read(filename)
204 filename = calling_fn.short_src
205 file = Files.read(filename)
206 if calling_fn.short_src:match("%.moon$") and type(MOON_SOURCE_MAP[file]) == 'table' then
207 local char = MOON_SOURCE_MAP[file][calling_fn.currentline]
208 line_num = file:line_number_at(char)
210 line_num = calling_fn.currentline
214 if file and filename and line_num then
216 local lines = file:lines()
217 for i = 1, line_num - 1 do
218 start = start + #lines[i] + 1
220 local stop = start + #lines[line_num]
221 start = start + #lines[line_num]:match("^ *")
222 error_message = pretty_error({
224 error = error_message,
232 if calling_fn.func == xpcall then
238 C('bold red', error_message or "Error"),
243 local _continue_0 = false
245 local calling_fn = debug_getinfo(level)
246 if not calling_fn then
249 if calling_fn.func == xpcall then
253 local name = calling_fn.name and "function '" .. tostring(calling_fn.name) .. "'" or nil
254 if calling_fn.linedefined == 0 then
257 if name == "function 'run_lua_fn'" then
263 local map = SOURCE_MAP and SOURCE_MAP[calling_fn.source]
265 if calling_fn.currentline then
266 calling_fn.currentline = assert(map[calling_fn.currentline])
268 if calling_fn.linedefined then
269 calling_fn.linedefined = assert(map[calling_fn.linedefined])
271 if calling_fn.lastlinedefined then
272 calling_fn.lastlinedefined = assert(map[calling_fn.lastlinedefined])
274 local filename, start, stop = calling_fn.source:match('@([^[]*)%[([0-9]+):([0-9]+)]')
276 filename, start = calling_fn.source:match('@([^[]*)%[([0-9]+)]')
279 if calling_fn.name then
280 name = "action '" .. tostring(calling_fn.name:from_lua_id()) .. "'"
284 local file = Files.read(filename)
285 local lines = file and file:lines() or { }
287 local err_line = lines[calling_fn.currentline]
289 local offending_statement = C('bright red', err_line:match("^[ ]*(.*)"))
290 line = C('yellow', tostring(filename) .. ":" .. tostring(calling_fn.currentline) .. " in " .. tostring(name) .. "\n " .. tostring(offending_statement))
292 line = C('yellow', tostring(filename) .. ":" .. tostring(calling_fn.currentline) .. " in " .. tostring(name))
298 local search_level = level
299 local _info = debug.getinfo(search_level)
301 search_level = search_level + 1
302 _info = debug.getinfo(search_level)
307 local varname, val = debug.getlocal(search_level, i)
311 if val == calling_fn.func then
312 name = "local '" .. tostring(varname) .. "'"
313 if not varname:match("%(") then
319 for i = 1, _info.nups do
320 local varname, val = debug.getupvalue(_info.func, i)
324 if val == calling_fn.func then
325 name = "upvalue '" .. tostring(varname) .. "'"
326 if not varname:match("%(") then
336 file = Files.read(calling_fn.short_src)
341 if file and (calling_fn.short_src:match("%.moon$") or file:match("^#![^\n]*moon\n")) and type(MOON_SOURCE_MAP[file]) == 'table' then
342 local char = MOON_SOURCE_MAP[file][calling_fn.currentline]
343 line_num = file:line_number_at(char)
344 line = C('cyan', tostring(calling_fn.short_src) .. ":" .. tostring(line_num) .. " in " .. tostring(name or '?'))
346 line_num = calling_fn.currentline
347 if calling_fn.short_src == '[C]' then
348 line = C('green', tostring(calling_fn.short_src) .. " in " .. tostring(name or '?'))
350 line = C('blue', tostring(calling_fn.short_src) .. ":" .. tostring(calling_fn.currentline) .. " in " .. tostring(name or '?'))
355 local err_line = lines[line_num]
357 local offending_statement = C('bright red', tostring(err_line:match("^[ ]*(.*)$")))
358 line = line .. ("\n " .. offending_statement)
364 table.insert(ret, line)
365 if calling_fn.istailcall then
366 table.insert(ret, C('dim', " (...tail calls...)"))
370 if not _continue_0 then
374 return table.concat(ret, "\n")
379 ok, err = xpcall(fn, enhance_error)
388 enhance_error = enhance_error,
389 print_error = print_error